Errors and Error Handling

4 min read source

Errors and Error Handling

TL;DR

Always throw Error objects (or subclasses) — never strings — so you get a .stack and instanceof works. Build custom error classes by extending Error for typed handling. Use the ES2022 cause option to chain errors without losing the original. Know the transpilation gotcha that breaks instanceof on subclassed errors, and the difference between sync try/catch and async error propagation.

Interview Q&A

Q: Why throw Error instead of a string/object?

A: throw "boom" gives the catcher a bare string — no stack trace, no name, and instanceof Error is false. An Error captures a stack at construction and integrates with reporters and unhandledrejection. Throwing non-Errors is a code smell.

Q: How do you make a custom error class?

A:

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";   // so it prints as ValidationError, not Error
    this.field = field;
  }
}

try {
  throw new ValidationError("Email invalid", "email");
} catch (e) {
  if (e instanceof ValidationError) handleField(e.field);
  else throw e;                       // rethrow what you don't own
}

Set this.name explicitly — it doesn’t default to the class name. Add structured fields (field, code, statusCode) for typed handling instead of string-matching e.message.

Q: What is the cause option (ES2022)?

A: It chains a low-level error to a higher-level one without discarding context:

try {
  await db.query(sql);
} catch (err) {
  throw new Error("Failed to load user", { cause: err });  // preserves the original
}

The catcher can inspect e.cause for the root cause; reporters print both. Before cause, people stuffed the original into the message or a custom field — cause standardizes it.

Q: What’s the instanceof gotcha with subclassed errors?

A: When targeting old environments, transpilers (Babel/TS down-level to ES5) couldn’t properly subclass the built-in Error, so new MyError() instanceof MyError could be false. The fix is Object.setPrototypeOf(this, MyError.prototype) in the constructor — or simply target ES2015+ where extends Error works natively (the common modern case). Worth naming because it explains “why does my custom error catch as a plain Error?”

Q: Error.captureStackTrace?

A: A V8-only API to control where the stack starts, hiding framework frames so the trace points at the user’s call site:

function assert(cond, msg) {
  if (!cond) {
    const err = new Error(msg);
    if (Error.captureStackTrace) Error.captureStackTrace(err, assert); // omit `assert` itself
    throw err;
  }
}

Used by assertion/validation libraries to keep traces clean. Not available in Safari/Firefox — feature-detect.

Q: How does error handling differ sync vs async?

A:

  • Sync: try/catch catches throws in the same call stack.
  • async/await: try/catch works because await turns rejections into throws.
  • Promises without await: a try/catch will not catch a rejection from a promise you didn’t await — it escapes to unhandledrejection (promises-advanced.md).
  • Callbacks/timers: a throw inside a setTimeout callback can’t be caught by the try/catch around the setTimeout call — different tick, different stack.
try { setTimeout(() => { throw new Error("x"); }, 0); } catch (e) {} // does NOT catch

Q: What is AggregateError?

A: The error type Promise.any rejects with when all inputs reject — it bundles them in .errors. You can also throw it yourself to represent multiple failures (e.g., a batch where several items failed).

Gotchas / edge cases

  • finally can swallow errors — a return or throw inside finally overrides whatever the try/catch was doing. Keep finally to cleanup.
  • Catching and ignoring (catch (e) {}) hides bugs — at minimum log; rethrow what you don’t handle.
  • catch without a binding (catch {}) is valid ES2019 when you don’t need the error — but usually you do.
  • Errors lose their type across postMessage/structured cloneError is cloneable but custom fields/subclass identity may not survive worker boundaries; serialize intentionally.
  • error.message is for humans — branch on error.code/instanceof, not on message text (messages get reworded, localized).
  • Rejecting with a non-Error in your own promise propagates a value with no stack — construct an Error.

What a senior is expected to say

  • “Throw Error subclasses with a set name and structured fields; branch on type/code, not message text.”
  • cause chains errors without losing the original; reporters show the full chain.”
  • try/catch won’t catch throws from a setTimeout or an un-awaited promise — those go to unhandledrejection.”
  • Error.captureStackTrace is V8-only for clean assertion traces; the old subclass-instanceof break is a transpilation artifact fixed by setPrototypeOf or targeting ES2015+.”

Cross-references

Further reading