learn
JavaScript

async/await and error handling

Straight-line async code, try/catch, and the parallel patterns.

async/await writes asynchronous code that reads top-to-bottom: awaits for sequencing, try/catch for errors. Interviewers ask this because the syntax looks synchronous while the pitfalls (serial loops, uncaught rejections, fire-and-forget) are anything but.

Real-life situation

A settings save retries a flaky endpoint with backoff: try, wait a little longer each time, give up after three attempts with a real error. Written with await, the retry loop reads like its flowchart. Written with nested callbacks, the same logic hides behind three indent levels.

Visual

Await parks the functionbrowser

Call stack

script

Tasks

empty

Microtasks

empty

Console

Render the page shell

Print "Render the page shell". console.log prints "Render the page shell".Why: The current operation executes synchronously and appends this value.

Step 1 of 7
scenario.js
async function getWeather() {
  const data = await Promise.resolve({ condition: "Light rain" });
  console.log("Show the weather: Light rain");
}
console.log("Render the page shell");
getWeather();
console.log("Keep the page interactive");

Try it

Retry with backoff
javascriptTab moves to the next control
async function fetchWithRetry(url, tries) {
let attempt = 0;
while (attempt < tries) {
  try {
    const res = await fakeFetch(url, { latencyMs: 5 });
    return res;
  } catch (error) {
    attempt = attempt + 1;
    await sleep(5);
  }
}
throw new Error("gave up");
}
const res = await fetchWithRetry("/flaky", 3);
console.log(res.ok);
const data = await res.json();
console.log(data.ok);
Press Run to see the output.
Expected output
true
true
Runner helpers

sleep(ms) waits; fakeFetch(url) returns deterministic sample data. Console code runs in a worker with a 5-second limit.

Try dropping tries to 2 and watch it give up. Then retry /missing and explain why backoff never saves a 404.

When await helps, when it hurts

Await for sequencing and error handling. Never await independent calls one by one — launch them all, gather once. And never fire-and-forget an async call in reviewed code: return it or catch it.

Await in a loop — guess first, then reveal.

predict.js
async function run() {
const out = [];
for (const n of [1, 2]) {
  out.push(await Promise.resolve(n * 2));
}
return out;
}
run().then(function (out) {
console.log(out[0] + "," + out[1]);
});

Interview questions

What does the async keyword do to a function?basic

It guarantees the function returns a promise: plain return values become fulfilled promises, thrown errors become rejections. It also unlocks await inside. That is ALL it does — no threads, no parallelism, just a promise wrapper plus syntax permission. Every "async makes it faster" misconception dies once this lands.

Interviewers also ask

  • Does async make code run in parallel?
How do you handle errors with async/await?basic

try/catch around the awaits — a rejected awaited promise throws at that line, like synchronous code. One try can guard several awaits; finally cleans up either way. For independent calls, catch per-call or the first rejection masks the rest (prefer allSettled when every outcome matters). The readability win over chains is real, but the error rules are identical underneath.

Interviewers also ask

  • Where does a rejected await throw?
Why is awaiting inside a loop usually a mistake?intermediate

It serializes independent work: each iteration waits for the last, turning five 100ms fetches into 500ms. Worse, one rejection aborts the remaining iterations. Fix: map items to promises FIRST (all start together), then await Promise.all — or allSettled for best-effort batches. Keep in-loop await only when order or side effects genuinely demand it, and say why out loud.

Interviewers also ask

  • When is in-loop await actually correct?
How do you fetch a list of URLs with limited parallelism?intermediate

Start a fixed pool of workers (say 3), each looping: take the next URL, fetch it, store the result. Promise.all on the workers waits for everything while never exceeding the limit. Full Promise.all on fifty URLs hammers the server; serial await wastes time — the pool is the professional middle. Name it confidently; it is a standard interview follow-up to "fetch these in parallel".

Interviewers also ask

  • Why not Promise.all on all fifty?
Can you use await at the top level of a file?basic

In ES modules, yes — top-level await pauses module evaluation until the value settles, which is perfect for config and feature detection. In classic scripts and CommonJS, no: wrap in an async IIFE or .then chain. The gotcha is importers waiting on you — a slow top-level await delays everything downstream, so keep it fast and intentional.

Interviewers also ask

  • What waits when a module top-level-awaits?
How do you retry a failing async operation with backoff?intermediate

Loop attempts: try the call, on failure wait an increasing delay (500ms, 1s, 2s…) and retry, giving up after N tries or on non-retryable errors (don't retry a 400 like a timeout). Backoff (plus jitter in production) stops retry storms from crushing a struggling server. The shape — attempt, catch, sleep, escalate — is worth memorizing verbatim.

Interviewers also ask

  • Which errors should never be retried?
How do you cancel a fetch?intermediate

With AbortController: pass signal into fetch, call controller. abort() to reject it with an AbortError. The canonical use is superseded requests — a new search keystroke aborts the previous fetch so slow responses can't overwrite fresh ones. Always .catch the abort separately from real errors, or every keystroke logs a scary (and fake) failure.

Interviewers also ask

  • How do you stop a slow response overwriting a fresh one?
What is the difference between returning a value and returning a promise inside async?basic

Almost none to the caller — both arrive as a fulfilled promise either way. The difference is TIMING and flattening: returning a promise makes the outer promise adopt it (waiting for nested async work), while a plain value settles immediately. Forgetting return in either case hands the caller undefined, the async twin of the classic chain bug.

Interviewers also ask

  • What does the caller receive in each case?
How do Promise.all and async/await combine for a dashboard load?intermediate

Fire every independent fetch WITHOUT awaiting (collecting promises), then await Promise.all once — one wait, full parallelism, results in order. Wrap in try/catch for the all-or-nothing case, or allSettled when partial data still renders. Narrate it as two phases (launch, then gather); interviewers listen for exactly that split.

Interviewers also ask

  • What if one widget may fail independently?
What happens to errors you forget to catch in an async function?intermediate

The returned promise rejects, and if nobody handles it, that's an unhandled rejection — a crash in Node, a loud warning in browsers. An async function with no try/catch is only safe when every caller handles rejection. Habit: await inside try, or return the promise to a caller you trust — never fire-and-forget an async call in reviewed code.

Interviewers also ask

  • Is fire-and-forget ever acceptable?

Key takeaways

  • async wraps returns in promises; await parks only its function.
  • Rejected awaits throw at that line — guard with try/catch.
  • Launch independent work together; gather with all or allSettled.

References

Last verified: 2026-10-12.

On this page