Error Handling Patterns

Error types, try/catch/finally semantics, async error propagation, custom error classes, and the patterns that prevent silent failures.

must medium โฑ 18 min errorstry-catchasync-errorscustom-errorserror-propagation
Mastery:
Why interviewers ask this
Error handling is where juniors differ from seniors โ€” juniors catch errors, seniors design for them. Interviewers look for understanding of async error propagation and the ability to design resilient systems.

The Error object hierarchy

JavaScript has several built-in error types โ€” all extend Error:

Error
โ”œโ”€โ”€ SyntaxError     โ€” code couldn't be parsed
โ”œโ”€โ”€ ReferenceError  โ€” accessing an undeclared variable
โ”œโ”€โ”€ TypeError       โ€” wrong type for operation (null.property)
โ”œโ”€โ”€ RangeError      โ€” value outside valid range (new Array(-1))
โ”œโ”€โ”€ URIError        โ€” bad URI encoding/decoding
โ””โ”€โ”€ EvalError       โ€” (legacy, rarely seen)

Every Error has name, message, and stack. stack is non-standard but universally implemented.

try / catch / finally semantics

Run it yourself
Edit and run. Output is captured from console.log. Async logs (setTimeout/Promise) appear in real execution order.
Console
 

finally always runs โ€” even if the try block returns, or catch throws. Use it for cleanup (closing connections, hiding spinners). If finally has a return, it overrides any earlier return.

Async error propagation

The key insight: async/await unifies error handling between sync and async code โ€” await converts rejections into thrown exceptions.

Run it yourself
Edit and run. Output is captured from console.log. Async logs (setTimeout/Promise) appear in real execution order.
Console
 
// async/await โ€” identical semantics, cleaner syntax
async function fetchData(url) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    return await res.json();
  } catch (err) {
    // NetworkError from fetch, HTTP errors we threw, JSON parse errors โ€” all caught here
    console.error('fetch failed:', err);
    throw err; // re-throw so caller knows it failed
  }
}
The missing await trap
async function bad() {
  try {
    return fetch('/api/data'); // โ† missing await!
  } catch (err) {
    // This catch will NEVER trigger โ€” the rejection happens after return
  }
}

Without await, the try/catch wraps only the synchronous call to fetch(), not the async operation. The rejection propagates outside the function entirely. Always await inside try.

Handling parallel async work

// Promise.all โ€” one failure fails everything (fail-fast)
try {
  const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);
} catch (err) {
  // if either fails, we land here โ€” we don't know which one failed
}

// Promise.allSettled โ€” never rejects; you inspect each result
const results = await Promise.allSettled([fetchUser(id), fetchPosts(id)]);
for (const result of results) {
  if (result.status === 'fulfilled') process(result.value);
  else log(result.reason); // per-item error, doesn't abort the others
}

Custom Error classes

Extend Error to add domain-specific context and enable instanceof checks:

class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
  ) {
    super(message);
    this.name = this.constructor.name; // 'AppError'
    // Fix prototype chain in TypeScript when targeting ES5
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

class ValidationError extends AppError {
  constructor(message: string, public readonly field: string) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 'NOT_FOUND', 404);
  }
}

// In a handler:
try {
  await processRequest(data);
} catch (err) {
  if (err instanceof ValidationError) {
    res.status(400).json({ error: err.message, field: err.field });
  } else if (err instanceof NotFoundError) {
    res.status(404).json({ error: err.message });
  } else {
    res.status(500).json({ error: 'Internal server error' });
    logger.error(err); // log the unexpected one
  }
}

Unhandled rejections

A rejected promise with no .catch() (or try/catch) is an unhandled rejection. In browsers it fires window.unhandledrejection; in Node.js it fires process.on('unhandledRejection') and will terminate the process in newer versions.

// Global safety net (last resort โ€” still fix the root cause)
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled rejection at:', promise, 'reason:', reason);
  process.exit(1);
});

The Result pattern (TypeScript)

In TypeScript, some teams prefer a discriminated union return type over throw, making errors part of the functionโ€™s type contract:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

async function safeParseUser(id: string): Promise<Result<User>> {
  try {
    const user = await db.findUser(id);
    return { ok: true, value: user };
  } catch (err) {
    return { ok: false, error: err as Error };
  }
}

const result = await safeParseUser(id);
if (result.ok) {
  render(result.value);  // TypeScript knows value is User
} else {
  showError(result.error.message);
}

Say it out loud
โ€œtry/catch/finally catches thrown values; finally always runs for cleanup. In async code, await converts rejections to thrown exceptions โ€” but only if you actually await inside the try. Promise.allSettled is the right choice when you want each result independently regardless of failures. I create custom error subclasses to add typed context and enable instanceof discrimination in handlers. Unhandled rejections crash Node processes in modern versions, so every promise chain needs a .catch or an encompassing try/catch.โ€

Likely follow-up questions
  • How does error handling differ between .catch() and try/catch with await?
  • What does finally do if both the try block and a return inside catch both execute?
  • How do you handle errors in Promise.all when some fail and some succeed?
  • Why might you create a custom Error class?
  • What is an unhandled promise rejection?

References