Error Handling
Error types, Custom errors 🟢, Async errors ⚠️ 🎯
try {
risky();
} catch (err) { // ES2019+: catch binding is optional: `catch {`
console.error(err.message);
} finally {
cleanup(); // always runs (even on return/throw)
}
throw new Error("boom"); // throw anything, but prefer Error objects
Error types#
Error, TypeError, RangeError, ReferenceError, SyntaxError, URIError, EvalError, and AggregateError (from Promise.any). Each has .name, .message, .stack, and (modern) .cause.
Custom errors 🟢#
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
try {
throw new ValidationError("email required", "email");
} catch (e) {
if (e instanceof ValidationError) console.log(e.field);
}
// error chaining (ES2022)
throw new Error("failed to load config", { cause: originalError });
Async errors ⚠️ 🎯#
// try/catch does NOT catch errors from a rejected promise you didn't await
try { doAsync(); } catch {} // ❌ won't catch async rejection
try { await doAsync(); } catch {} // ✅ awaited → caught
// .catch for promise chains
fetch(url).then(r => r.json()).catch(err => /* handle */);
// finally on promises
p.finally(() => spinner.hide());
Unhandled rejections: browser fires window.onunhandledrejection; Node emits unhandledRejection (and by default crashes on it in recent versions).
Best practices: throw Error subclasses, never swallow errors silently, add context via cause, fail fast, handle at the boundary (route/UI layer), and don't use exceptions for normal control flow.
Interview Q&A#
Q1. Does finally run if try returns? If it throws?
Yes to both — finally always runs, including on return, throw, or break out of the block. A return or throw inside finally will even override one from the try/catch, so avoid returning from finally.
Q2. Why doesn't try/catch catch an un-awaited async error?
A rejected promise you don't await settles later, after the synchronous try block has already exited, so there's no active try/catch frame to catch it. await the call (or attach .catch) so the rejection surfaces inside the block: try { await doAsync(); } catch {}.
Q3. How do you create and detect a custom error type?
Subclass Error, call super(message), and set this.name; add any custom fields. Detect it with instanceof:
class ValidationError extends Error {
constructor(message, field) { super(message); this.name = "ValidationError"; this.field = field; }
}
if (e instanceof ValidationError) { /* ... */ }