Engrain by ByteRush

JS.11.02 Mid-level· 11 minCore — asked often

Error handling in async code: where errors go, and where they get lost

Place catch and finally correctly, avoid forgotten awaits, know why return await matters inside try, and what browsers and Node.js do with unhandled rejections.

Comes up in: JavaScript, Coding, Debugging

Learn#

Arjun’s profile page shows a blank screen when the API is down. He wrote a try/catch with a fallback name, but the fallback never appears.

Inside an async function, await turns a rejected promise into a thrown error, so try/catch can handle it. Without await, there is nothing to catch.

const fail = async () => { throw new Error("503"); };
async function withoutAwait() {
  try { return fail(); } catch { return "fallback"; }
}
async function withAwait() {
  try { return await fail(); } catch { return "fallback"; }
}
try { console.log(await withoutAwait()); }
catch (e) { console.log("escaped:", e.message); }
console.log(await withAwait());

Output (verified)

escaped: 503
fallback

return fail() leaves the try block before the promise rejects.

Why return await matters inside tryStep 1 of 4

Call stack

  • withoutAwait
  • try { return fail() }

State

    empty

    Console

      empty

      withoutAwait() enters try and calls fail(). fail returns a promise that will reject.

      Read the steps as text
      1. withoutAwait() enters try and calls fail(). fail returns a promise that will reject.
      2. return hands that promise out at once. The function leaves try, so its catch is no longer active.
      3. fail()'s promise rejects. The catch block never sees it; the caller gets the error.
      4. With return await, the function waits inside try. The rejection becomes a throw there, and catch runs.

      Error propagation is how an error travels up to a handler. In async code it travels along promises, so every promise must be awaited or returned to someone who handles it.

      Predict#

      Predict first

      A Node.js 26 script runs Promise.reject(new Error('x')) and has no handler anywhere. What happens?

      Interview angle#

      Say it like this — about 30 seconds

      “Rejections travel along promises, so a catch only works if it waits on the right promise. Inside try, I write return await, otherwise the function returns before the promise rejects and catch never runs. finally runs either way and should not return a value. In the browser, an unhandled rejection fires the unhandledrejection event; in Node.js since version 15 it crashes the process by default. I also watch for forgotten awaits, such as async callbacks passed to forEach.”

      Stays on your device. Nothing is uploaded.
      Follow-up What happens to an unhandled rejection in the browser?
      The page keeps running. The window gets an unhandledrejection event, and the error is logged to the console.
      Follow-up return promise vs return await promise inside try?
      Only return await lets that try’s catch and finally see the rejection; outside try they behave the same.
      Follow-up Why is forEach with an async callback a bug?
      forEach ignores the returned promises, so nothing waits for them and their errors are unhandled. Use for...of with await, or Promise.all.

      Practice#

      On your own

      Fix four real bugs where an error is swallowed, escapes the handler, or skips cleanup.

      Done when

      • getName returns “guest” when the API rejects.
      • saveAll resolves only after every save, and rejects if one fails.
      • loadUser logs the error and still rejects with the same error.
      • withLock calls release exactly once, on success and on failure.
      Your solution
      Ctrl/⌘ + Enter

      Remember#

      Try to answer each card before you open it. Then add them to your reviews.

      Inside try, why use return await instead of return?

      So the rejection happens inside try, and catch or finally can see it.

      What does Node.js do with an unhandled rejection by default?

      Crashes the process with exit code 1 (since Node.js 15).

      Which browser event fires for an unhandled rejection?

      unhandledrejection, on window.

      Why does items.forEach(async ...) lose errors?

      forEach ignores the returned promises, so nobody waits for or handles them.

      Go deeper

      Four layers. Stop at the one your interview needs; each ends with a quick check.

      The core ideaEvery round
      A catch handles errors from everything before it in the chain. Put it right after a step to give that step a fallback; put it at the end to handle any failure once.
      A forgotten await is one of the most common async bugs. The code runs on with a pending promise, and its error has no handler.
      Check yourself: Where should a catch go if you want a fallback for one step only?

      Directly after that step, before the later steps in the chain.

      How it works underneathSenior rounds
      finally runs after success or failure and passes the original result through. If finally itself returns a value or throws, that replaces the original result or error, and an error can be lost. Use it only for cleanup.
      Check yourself: What happens if finally returns a value?

      It replaces the result or swallows the error of the try block.

      Edge cases and trapsWhere people slip
      An unhandled rejection is reported when a promise rejects with no handler by the end of the current microtask round. Browsers fire unhandledrejection and, if a handler comes later, rejectionhandled Source: MDN: unhandledrejection event. Node.js crashes by default since 15.0.0 Source: Node.js: --unhandled-rejections.
      Check yourself: Can a rejection that is handled later still be reported as unhandled?

      Yes. If no handler is attached by the time the microtasks finish, it is reported; a later handler fires rejectionhandled.

      In productionStaff-level answers

      Turn on the no-floating-promises and no-misused-promises lint rules from typescript-eslint: they catch forgotten awaits and async callbacks passed to forEach. Report unhandled rejections to your error tracker (JS.13.02), and catch errors where you can show the user something useful.

      Check yourself: Which lint rule catches promises nobody awaits?

      @typescript-eslint/no-floating-promises.

      Finished the five steps? Mark it, and its cards join your reviews.

      Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

      Interview questions this answers

      • What happens to an unhandled promise rejection in the browser and in Node?
      • What is the difference between return promise and return await promise inside try?