Error handling
try / catch / finally for synchronous flows; .catch() or
try / await for promises. Always throw Error (or a subclass),
never strings. Custom error classes carry semantic meaning and unlock matching.
1 · try / catch / finally
function parseAge(s) {
const n = Number(s);
if (Number.isNaN(n)) throw new TypeError("not a number: " + s);
if (n < 0) throw new RangeError("negative age: " + n);
return n;
}
try {
console.log(parseAge("twenty"));
} catch (err) {
console.log(err.name, "-", err.message);
} finally {
console.log("cleanup runs either way");
}finally runs whether the try succeeds, throws, or returns early.
Use it for resource cleanup — closing files, releasing locks, restoring state.
2 · The Error type and its subclasses
| Built-in | For |
|---|---|
Error | Generic. Use when none of the others fit. |
TypeError | Value of the wrong type — null where object expected, non-function called. |
RangeError | Numeric out of range — stack overflow, invalid array length. |
SyntaxError | Parse error. Usually JSON.parse or eval. |
ReferenceError | Variable not declared. |
AggregateError | Multiple errors at once — from Promise.any. |
Each carries name, message, and a stack trace. The type alone tells the caller something.
3 · Custom error classes
class ValidationError extends Error {
constructor(field, reason) {
super(`validation failed: ${field} (${reason})`);
this.name = "ValidationError";
this.field = field;
this.reason = reason;
}
}
class NotFoundError extends Error {
constructor(resource) {
super(`not found: ${resource}`);
this.name = "NotFoundError";
this.resource = resource;
}
}
function fetchUser(id) {
if (!id) throw new ValidationError("id", "required");
if (id < 0) throw new ValidationError("id", "must be positive");
if (id > 1000) throw new NotFoundError("user/" + id);
return { id, name: "Alice" };
}
try {
fetchUser(9999);
} catch (err) {
if (err instanceof NotFoundError) {
console.log("404:", err.resource);
} else if (err instanceof ValidationError) {
console.log("400:", err.field, err.reason);
} else {
throw err;
}
}instanceof-match and handle each
class differently. Stringly-typed errors lose this; checking err.message
against a substring is fragile and reads badly in code review.4 · Errors in async code
async function load() {
throw new Error("boom");
}
// With await — try/catch works
async function main() {
try {
await load();
} catch (err) {
console.log("caught via await:", err.message);
}
}
// With .then — use .catch
load().catch(err => console.log("caught via .catch:", err.message));
main();A rejected promise is an error path. Always handle it — either with
.catch or a surrounding try / await. Unhandled rejections
are eventual process-killers in Node and "Uncaught (in promise)" warnings in the browser.
5 · The cause property (ES2022)
async function load(id) {
try {
await fetch("/api/" + id); // fictional — pretend this throws
} catch (err) {
throw new Error("failed to load " + id, { cause: err });
}
}
(async () => {
try {
await load(42);
} catch (err) {
console.log("outer:", err.message);
console.log("inner:", err.cause?.message);
}
})();new Error(msg, { cause }) chains the original error. Preserves stack
context across re-throws without losing the root cause. Use it at layer boundaries.
6 · Patterns to use
- Throw early, catch late. Validate at the entry point; let the rest of the function assume valid inputs. Catch where you can do something useful (log, retry, return fallback).
- Catch specific, rethrow unknown.
if (err instanceof MyError) handle(); else throw err;— never silently swallow. - Convert at boundaries. A library catches low-level errors and rethrows domain errors.
DatabaseErrorwraps aconnection refused. - finally for cleanup, not for return values. A
returnin finally overrides thetry's return value — almost always a bug.
7 · Common mistakes
throw "bad"— strings have no stack trace, noname. Always throwError.- Swallowing errors.
catch (e) {}hides bugs. At least log; ideally re-throw or handle. - Missing
.catchon a fire-and-forget promise. Usevoid load()or attach a.catchfor logging. - Catching too wide.
catch (e)grabs everything — including bugs you should let crash. Match specific types. - Throwing from a setTimeout / setInterval callback. Doesn't propagate to your
try / catch. Wrap the callback body itself intry.
8 · When it clicks
- You define custom error classes by reflex for new modules.
- You reach for
causewhen wrapping a lower-level error. - You spot bare
catch {}and unhandled promise chains in code review. - You stop trying to recover from
TypeError/RangeError— those are bugs, not conditions.