A browser module retries a failed API call, shows each waiting state, and renders the result after the third response succeeds.

Program

The source loops through a small retry budget. Failed responses update the attempt list and wait before the next request; the successful response supplies the data for the final UI state.

fetch_retry_backoff.js
Visuals: captured from real browser rendering
const panel = document.querySelector('#sync-panel');
const attempts = document.querySelectorAll('.attempt');
const result = document.querySelector('#sync-result');

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

let report;

for (let attempt = 1; attempt <= 3; attempt += 1) {
  const response = await fetch('/api/sync');

  if (response.ok) {
    report = await response.json();
    attempts[attempt - 1].dataset.state = 'ok';
    attempts[attempt - 1].textContent = 'success';
    break;
  }

  attempts[attempt - 1].dataset.state = 'retry';
  attempts[attempt - 1].textContent = `retry in ${attempt * 250}ms`;
  await wait(attempt * 250);
}

panel.dataset.state = 'ready';
result.textContent = `${report.items} items synced`;
  1. Attempt 1 requests the sync endpoint.

    Three pending sync attempts before any retry state is painted
    The first failed response is known in JavaScript, but the page still shows all attempts as pending.
  2. Check whether attempt 1 succeeded.

    The sync panel remains unchanged after checking the first failed response
    The condition is false, so execution falls through to the retry branch.
  3. Mark attempt 1 as a retry.

    Attempt 1 turns orange while attempts 2 and 3 remain pending
    A failed response becomes visible when the first attempt card changes state.
  4. Render the first backoff delay.

    Attempt 1 shows retry in 250ms
    The first delay is short because the attempt number is 1.
  5. Wait before trying again.

    Attempt 1 remains in its retry state during the first wait
    Waiting changes time, not pixels, so the browser output stays the same.
  6. Attempt 2 requests the same endpoint.

    Attempt 1 is retrying while attempt 2 has just received a failed response
    The second response is also a failure, but attempt 2 is not painted yet.
  7. Mark attempt 2 as a retry.

    Attempts 1 and 2 are orange while attempt 3 remains pending
    The second failed response adds a second visible retry card.
  8. Render the second backoff delay.

    Attempt 2 shows retry in 500ms
    The delay grows on the second failed attempt.
  9. Wait longer before the final retry.

    Attempts 1 and 2 remain in retry states during the second wait
    The longer wait still leaves the rendered state unchanged.
  10. Attempt 3 receives a successful response.

    Attempt 3 has received a successful response but is not painted yet
    The successful response exists before the green attempt card appears.
  11. Check whether attempt 3 succeeded.

    The panel remains unchanged after the successful condition is checked
    The true condition moves execution into the success branch before any DOM writes.
  12. Parse the successful JSON body.

    The sync panel is still waiting while report data is only in JavaScript
    Parsing the body creates data for the final render, but the page has not used it yet.
  13. Mark attempt 3 as successful.

    Attempt 3 turns green while its label still says pending
    The green state appears before the success text is written.
  14. Render the success label for the final attempt.

    Attempt 3 is green and says success
    The third attempt now visibly explains why the retry loop can stop.
  15. Break out of the retry loop.

    The three attempt cards remain unchanged when the loop exits
    Breaking changes control flow, not the rendered output.
  16. Mark the whole sync panel as ready.

    The sync panel header turns green after the retry loop succeeds
    The page-level state changes after the loop has completed.
  17. Render the successful sync result.

    The final sync panel says 42 items synced
    The final frame uses data from the successful third response.
retry budget A retry loop should have a fixed limit so the browser does not keep requesting forever.
backoff delay Backoff gives the service time to recover before the next request. This example shows the delay value in the UI.
response sequence Each response changes what the next step can do: failed responses keep the loop moving, while a successful response breaks out.
final render The page should update only after the successful response has been parsed into usable data.