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/catchcatches throws in the same call stack. async/await:try/catchworks becauseawaitturns rejections into throws.- Promises without
await: atry/catchwill not catch a rejection from a promise you didn’t await — it escapes tounhandledrejection(promises-advanced.md). - Callbacks/timers: a throw inside a
setTimeoutcallback can’t be caught by thetry/catcharound thesetTimeoutcall — 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
finallycan swallow errors — areturnorthrowinsidefinallyoverrides whatever thetry/catchwas doing. Keepfinallyto cleanup.- Catching and ignoring (
catch (e) {}) hides bugs — at minimum log; rethrow what you don’t handle. catchwithout a binding (catch {}) is valid ES2019 when you don’t need the error — but usually you do.- Errors lose their type across
postMessage/structured clone —Erroris cloneable but custom fields/subclass identity may not survive worker boundaries; serialize intentionally. error.messageis for humans — branch onerror.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
Errorsubclasses with a setnameand structured fields; branch on type/code, not message text.” - “
causechains errors without losing the original; reporters show the full chain.” - “
try/catchwon’t catch throws from asetTimeoutor an un-awaited promise — those go tounhandledrejection.” - “
Error.captureStackTraceis V8-only for clean assertion traces; the old subclass-instanceofbreak is a transpilation artifact fixed bysetPrototypeOfor targeting ES2015+.”
Cross-references
- Promise rejection tracking (
unhandledrejection): promises-advanced.md - Event-loop reasoning for “why try/catch misses the timer”: event-loop.md
- React error boundaries (component-tree errors): ../05_react/
- Frontend error reporting / observability: ../15_performance/
Further reading
- MDN —
Error: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - MDN — Error
cause: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause - V8 — Stack trace API: https://v8.dev/docs/stack-trace-api