Q27 of 40 · JavaScript

How should you handle errors in async JavaScript code?

JavaScriptMidjavascripterror-handlingasyncpromisestry-catch

Short answer

Short answer: Use `try/catch` around `await` expressions for async/await code. For Promise chains, attach `.catch()` at the end. In Node.js, handle `unhandledRejection` and `uncaughtException` process events. Always rethrow or log — swallowing errors silently is the most dangerous anti-pattern.

Detail

Async error handling requires explicit patterns — errors do not propagate to the caller's stack like synchronous exceptions.

async/await with try/catch: Wrapping await in try/catch is the cleanest approach. Be specific about what you catch — a broad catch block can mask unrelated errors.

Promise chain .catch(): Attach .catch() at the end of the chain to handle any rejection that propagated through the chain. .catch(fn) is equivalent to .then(undefined, fn).

Granular vs. global error handling:

  • Granular: wrap each logical unit; re-throw if you can't handle the error locally
  • Global: Node.js process.on('unhandledRejection') and process.on('uncaughtException') as last-resort safety nets — never the primary strategy

In test automation: Test frameworks (Jest, Playwright) already catch and report rejections. But test helpers and custom fixtures should handle their own async errors and clean up resources in finally blocks regardless of outcome.

Error context: When rethrowing, wrap with context: throw new Error(Failed step: ${err.message}) — generic "Error" messages with no context make debugging painful.

// EXAMPLE

// async/await — try/catch
async function fetchUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    // Add context before rethrowing
    throw new Error(`fetchUser(${id}) failed: ${err.message}`);
  } finally {
    // cleanup always runs
    console.log("fetch attempt done");
  }
}

// Promise chain
fetch("/api/data")
  .then(r => r.json())
  .then(data => process(data))
  .catch(err => {
    logger.error("Pipeline failed", err);
    throw err; // rethrow — don't swallow
  });

// Anti-pattern — swallowed error
async function bad() {
  try {
    await riskyOp();
  } catch {} // ← silent failure, impossible to debug
}

// WHAT INTERVIEWERS LOOK FOR

try/catch for async/await, .catch() for chains, finally for cleanup. The distinction between handling and swallowing errors. Adding context when rethrowing. Connecting to test reliability — unhandled rejections in tests cause false passes.

// COMMON PITFALL

Empty catch blocks that swallow errors silently. Also: try/catch only catches errors in awaited Promises — a non-awaited Promise rejection inside try will escape the catch.