cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Async patterns

Learn the practical async patterns that keep apps calm: retry with backoff, timeout races, task pools, polling loops, and sleep helpers.

Patterns you’ll be able to build
  • 01
    Retry carefullyRetry only transient, safe operations with capped exponential backoff and jitter.
  • 02
    Control waitingRace work against a timeout and know why race alone does not cancel the loser.
  • 03
    Limit trafficUse pools and polling loops so async work happens steadily instead of all at once.

Async work needs traffic rules

async and await make asynchronous code read from top to bottom, but real applications still need patterns. A network can be slow, a server can be busy, a user can leave, and eight jobs can all want the same resource at once.

This lesson collects five patterns you will reuse constantly: retry with backoff, timeouts, queues and pools, polling, and sleep helpers. They all share one rule: waiting should be deliberate, bounded, and cancelable when possible.

The short version

An async pattern is a small, repeatable shape for waiting without blocking: try again later, stop waiting after a limit, keep only a few tasks in flight, ask for status until done, or pause with a promise-based timer.

If you want to review the building blocks, revisit Promises, Timers, Error handling strategies, and The event loop.

Real-life analogyAsync patterns are a road system

Roads work because everyone agrees on a few rules. Async code is similar. You can launch everything at once, retry forever, and poll every millisecond, but the app becomes noisy and unfair. Patterns are the traffic rules that keep work moving.

In real life: Speed limits
In JavaScript: Timeouts and total retry budgets
In real life: Traffic lights
In JavaScript: Pools that let only N tasks run
In real life: Detours
In JavaScript: Retries after temporary failures
In real life: Exit signs
In JavaScript: Abort signals and stop conditions

Where the analogy stops: Roads do not choose for you where to go. Async patterns do not make a flaky API correct; they only make your waiting safer and more predictable.

Retry with backoff

INTERACTIVE

A retry is another attempt after a failure. Backoff means the waits get longer: 100 ms, 200 ms, 400 ms, then perhaps capped at 2 seconds. With full jitter, you pick a random wait between 0 and that cap so every client does not retry at the same instant.

Real-life analogyRetry with backoff is calling a busy friend

If your friend is busy, redialing ten times per second is rude and useless. Waiting a little longer each time gives them room to become available. Jitter prevents every caller who got the same busy signal from redialing together.

In real life: Busy signal
In JavaScript: Temporary failure such as timeout, 503, or 429
In real life: Wait 1, then 2, then 4 minutes
In JavaScript: Exponential backoff: base × 2^attempt
In real life: Add a small random pause
In JavaScript: Jitter spreads clients out
In real life: Stop after a few tries
In JavaScript: Attempt and total-time caps

Where the analogy stops: A friend knows you personally. Servers see huge crowds, so polite retry code must also check whether the operation is safe to repeat.

Retry only when both conditions are true: the error is probably temporary, and repeating the operation is safe. Reads are usually safe. Writes need idempotency, such as a payment API accepting an idempotency key so repeated attempts do not charge twice.

Flaky server retry lab
Retry helperPop out in the code editor (opens in a new tab)JavaScript
async function retry(operation, options) {  for (let attempt = 0; attempt < options.maxAttempts; attempt += 1) {    try {      return await operation();    } catch (error) {      if (!error.temporary) throw error;      const uncapped = options.baseDelay * 2 ** attempt;      const delay = Math.min(uncapped, options.maxDelay);      const wait = options.random() * delay;      await options.sleep(wait);    }  }  throw new Error("No attempts left");}
Retry settings0 events
  1. Press Run to call the flaky server.
Try it yourself

Run a fake server that fails first. Temporary failures retry with capped backoff; permanent failures stop immediately. The random value is fixed here so the timeline is teachable.

Browser waits are intentionally short. Reset clears scheduled timers before they can update the lab.

The lab injects a fixed random function and short sleeps. Production code should do the same for tests: inject sleep and random, then assert delays and attempt counts instead of waiting for real time.

Retry or not?
  • Network timeout while reading a profile
  • 503 Service Unavailable
  • 429 Too Many Requests with Retry-After
  • 400 Bad Request
  • 401 Unauthorized
  • 404 Not Found
  • A payment POST with no idempotency key
Try it yourself
0 of 7 correct

Sort each failure. Imagine the request is otherwise well-formed and ask whether another identical attempt is safe.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Timeouts with race

STEP THROUGH

A timeout is a promise that rejects after a limit. Promise.race lets the real task and the timer compete. The first one to settle decides the wrapper’s result.

Real-life analogyA timeout race is a kitchen timer racing your cake

If the kitchen timer rings before the cake is done, you know you have waited long enough. But the timer does not magically remove the cake from the oven. Likewise, a timed-out race does not stop the promise that lost.

In real life: Cake in the oven
In JavaScript: The original async task
In real life: Kitchen timer
In JavaScript: A timeout promise
In real life: Whichever finishes first gets attention
In JavaScript: Promise.race settles with the first promise
In real life: The cake can keep baking
In JavaScript: Race does not cancel the losing task

Where the analogy stops: A human can pull the cake out. Promise.race alone cannot stop JavaScript work; the task must accept a cancellation signal.

Timeout with Promise.race
Step 0 of 7Ready
Your turn: follow the blue line

Step through a timeout wrapper. Change the setting to see a task beat the timer or a timer beat the task.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  let timer;  const timeout = new Promise((_, reject) => {    timer = setTimeout(() => reject(new Error("Timed out")), ms);  });  return Promise.race([promise, timeout])    .finally(() => clearTimeout(timer));}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Change the race

Plain race chooses a winner. The AbortSignal version gives the task a stop request.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Two details matter. First, clear the timeout in finally so the timer does not fire later or keep a Node process alive. Second, use an AbortController when the underlying task can cooperate. The previous lesson, Cancellation with AbortController, covers that pattern in detail.

Async queues & pools

INTERACTIVE

A pool keeps at most N tasks in flight. It is the difference between opening 100 network requests at once and allowing four workers to steadily drain a line. This protects the browser, the server, and the user’s battery.

Real-life analogyA task pool is a bank with three tellers

A bank with three tellers does not serve everyone at once. It serves up to three people, then calls the next ticket as each teller becomes free. A task pool follows the same shape.

In real life: Numbered-ticket line
In JavaScript: The queue of jobs
In real life: Three tellers
In JavaScript: Concurrency of 3
In real life: Next customer walks up
In JavaScript: Next task starts when a worker finishes
In real life: Receipt order
In JavaScript: Results can be stored by original input index

Where the analogy stops: Bank customers are people; tasks can fail. Your pool must decide whether to collect errors and continue or fail fast and stop launching new work.

Task pool: numbered-ticket line
async function runPool(tasks, concurrency) {  const results = [];  let next = 0;  async function worker() {    while (next < tasks.length) {      const index = next++;      results[index] = await tasks[index]();    }  }  await Promise.all(Array.from({ length: concurrency }, worker));  return results;}
LanesTotal 560 ms
Teller 1
  1. Job 1: 0–320 ms
  2. Job 7: 320–460 ms
Teller 2
  1. Job 2: 0–120 ms
  2. Job 4: 120–300 ms
  3. Job 6: 300–560 ms
Teller 3
  1. Job 3: 0–240 ms
  2. Job 5: 240–320 ms
  3. Job 8: 320–540 ms

Results: job 1, job 2, job 3, job 4, job 5, job 6, job 7, job 8

Try it yourself

At most 3 jobs run at once. Results stay in input order even when short jobs finish early.

The bars show a deterministic schedule, not actual wall-clock timers.

Decide the failure policy before you write the helper. Collect errors when independent jobs should all get a chance, such as image thumbnails. Fail fast when one failure makes the whole result useless, such as loading required configuration.

Polling

INTERACTIVE

Polling means checking again and again until a job is done, failed, aborted, or timed out. It is useful when the server cannot push updates yet. Later, push tools such as WebSocket or Server-Sent Events can act more like a doorbell.

Real-life analogyPolling is checking the mailbox

Without a doorbell, you might check the mailbox every few minutes. Checking every second wastes energy; checking forever is worse. Good polling is patient and has exit ramps.

In real life: Walk to the mailbox every few minutes
In JavaScript: Send a status request on an interval
In real life: Stop when the package arrives
In JavaScript: Return on done
In real life: Stop when the delivery is cancelled
In JavaScript: Throw on failed or abort
In real life: A doorbell would be nicer
In JavaScript: Push updates, covered in a later lesson

Where the analogy stops: Mailboxes do not overload your house. Servers can be overloaded, so poll slowly, back off when responses are slow, and stop when the page no longer needs the answer.

Polling loop: mailbox checks
Polling loopPop out in the code editor (opens in a new tab)JavaScript
async function pollJob(checkStatus, { interval, timeout, signal, sleep }) {  const deadline = Date.now() + timeout;  while (Date.now() < deadline) {    if (signal?.aborted) throw signal.reason;    const status = await checkStatus();    if (status.state === "done") return status.value;    if (status.state === "failed") throw new Error(status.reason);    await sleep(interval, { signal });  }  throw new Error("Polling timed out");}
Statusidle
  1. Press Start to check job status.
Try it yourself

This model waits for each check, then sleeps, then checks again. It is an async loop shape, not setInterval, so requests cannot pile up on top of each other.

Real apps also stop on success, failure, abort, or total timeout; many pause while the tab is hidden with document.visibilityState.

Prefer an async loop or nested setTimeout over setInterval for network polling. The loop can await the request and the sleep, so the next poll does not start while the previous one is still running.

sleep()

CODE

JavaScript does not have a built-in browser sleep function, but a timer can resolve a promise. Awaiting that promise pauses only your async function; the page and event loop keep working.

Real-life analogysleep() is a nap alarm

A nap alarm lets you pause yourself without stopping the whole household. await sleep(500) pauses one async function without blocking the browser.

In real life: Set an alarm
In JavaScript: Call setTimeout
In real life: Nap until it rings
In JavaScript: await sleep(ms) inside one async function
In real life: Cancel the alarm
In JavaScript: Abort signal clears the timer

Where the analogy stops: A real nap stops you completely. await sleep does not freeze JavaScript; other tasks, rendering, and user events continue.

A tiny sleep helperPop out in the code editor (opens in a new tab)JavaScript
const sleep = (ms) => new Promise((resolve) => {  setTimeout(resolve, ms);}); await sleep(500);console.log("half a second later");
Cancellable sleepPop out in the code editor (opens in a new tab)JavaScript
function sleep(ms, { signal } = {}) {  return new Promise((resolve, reject) => {    if (signal?.aborted) return reject(signal.reason);    const timer = setTimeout(resolve, ms);    signal?.addEventListener("abort", () => {      clearTimeout(timer);      reject(signal.reason);    }, { once: true });  });}

In Node.js, you can also import setTimeout from node:timers/promises and await it directly. In browser lessons, a small helper keeps the idea visible.

Where you’ll use this

Real front-end code often combines patterns. A draft save might retry a safe idempotent PUT, parse the response with a timeout, and stop if the user leaves the page.

Combining patternsPop out in the code editor (opens in a new tab)JavaScript
async function saveDraft(draft, api) {  const result = await retry(    () => api.putDraft(draft, { idempotencyKey: draft.id }),    { maxAttempts: 4, baseDelay: 100, maxDelay: 1000, sleep, random: Math.random },  );  return withTimeout(result.json(), 2000);}
Choosing an async pattern
NeedPatternStop condition
Temporary network failureRetry with backoffAttempt cap, total time, permanent error
User should not wait foreverTimeoutTimer wins, then cleanup and maybe abort
Many independent tasksPoolQueue empty, fail fast, or collected errors returned
Long server jobPolling loopDone, failed, abort, or timeout
Pause between attemptssleep(ms)Timer resolves or signal aborts

Common misconceptions

  • “Retry everything.” Retry transient errors only. A 400, 401, or unsafe payment write needs a fix, not another identical attempt.
  • “Backoff can run forever.” Always cap delay, attempts, and total time.
  • “Promise.race cancels the loser.” It settles the wrapper. The losing task keeps running unless it cooperates with cancellation.
  • “setInterval is perfect for polling.” It can overlap slow requests. An async loop waits before the next check.
  • “A pool changes result order.” Completion order changes, but you can store each result at its original index.
  • “sleep blocks JavaScript.” Awaiting a sleep promise pauses one async function, not the whole event loop.
Testing tip

Make helpers testable by injecting sleep and random. Unit tests can then assert “attempts were 1, 2, 3 and waits were 100, 200, 400” without depending on wall-clock time.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the backoff delays

Run the mental model first: what exact text reaches the console?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const waits = [];
for (let attempt = 0; attempt < 3; attempt += 1) {
  waits.push(100 * 2 ** attempt);
}
console.log(waits.join(", "));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeExplain the race

    Answer with yes or no: does Promise.race cancel the slower promise?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeKeep pool results ordered

      What does the starter print, and which line shape preserves this order in an async pool?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const finished = [];
      const jobs = ["A", "B", "C"];
      for (const name of jobs) {
        finished.push(name);
      }
      console.log(finished.join(" -> "));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeCount the polling loop

        Predict the output of this simplified polling counter.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        let polls = 0;
        while (polls < 3) {
          polls += 1;
        }
        console.log("polls", polls);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeDesign a safe retry policy

          Write down a retry policy for saving a profile form. Include what you retry, what you never retry, and how you stop.

            Check your understanding

            7 QUESTIONS
            Async patterns quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which retry delay formula is capped exponential backoff?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const waits = [];
              for (let attempt = 0; attempt < 3; attempt += 1) {
                waits.push(50 * 2 ** attempt);
              }
              console.log(waits.join("/"));

              Choose an answer to see the explanation.

            3. Question 3 of 7What does Promise.race([task, timeout]) do when the timeout wins?

              Choose an answer to see the explanation.

            4. Question 4 of 7How many waves does this pool need?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const concurrency = 3;
              const jobs = 8;
              console.log(Math.ceil(jobs / concurrency));

              Choose an answer to see the explanation.

            5. Question 5 of 7Why prefer an async polling loop over setInterval for requests?

              Choose an answer to see the explanation.

            6. Question 6 of 7Which operation is safest to retry?

              Choose an answer to see the explanation.

            7. Question 7 of 7What should a timeout helper do with its timer after the race settles?

              Choose an answer to see the explanation.

            Key takeaways

            • Retry only transient, safe operations; use exponential backoff, jitter, and caps.
            • Promise.race is a timeout wrapper, not cancellation. Clear timers and use signals when work must stop.
            • Pools keep at most N tasks in flight and should define how failures are handled.
            • Polling should be an async loop or nested timeout so checks do not overlap.
            • sleep is just a promise around a timer; make it cancellable when it may wait a long time.

            Async patterns are reusable waiting shapes that make asynchronous work bounded, polite, and predictable.

            Up next: The browser environment.

            CompleteFrontend Clear concepts. Working examples.