JavaScript Error Handling
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
Failures are normal. The dangerous case is a swallowed error: the program keeps running and the symptom shows up later as wrong data with no clue where it came from.
The Error object
const err = new Error("file not found");
err.name; // "Error"
err.message; // "file not found"
err.stack; // stack trace string
Creating an Error does nothing on its own. It has to be thrown.
Built-in types you will meet:
| Type | Typical trigger |
|---|---|
TypeError |
operation on the wrong type, e.g. null.name |
ReferenceError |
reading a variable that was never declared |
SyntaxError |
invalid code, also thrown by JSON.parse |
RangeError |
value out of range, e.g. new Array(-1) |
AggregateError |
several failures at once, e.g. Promise.any |
throw
function divide(a, b) {
if (b === 0) {
throw new Error("b must not be zero");
}
return a / b;
}
Throwing abandons the current function immediately and unwinds to the nearest catch. With no handler, Node prints the stack and exits; the browser logs it and stops that task.
Throw Error instances, not strings:
throw new Error("failed"); // keeps stack, name, instanceof
throw "failed"; // no stack, no type information
Throwing a string also breaks every caller that checks err instanceof Error.
try / catch / finally
try {
const data = JSON.parse(raw);
console.log(data);
} catch (err) {
console.error("parse failed:", err.message);
} finally {
console.log("always runs");
}
Order of execution:
- An exception inside
tryjumps tocatch; the rest oftryis skipped. finallyruns whether or not something threw, including whencatchreturns or rethrows.- If
finallyitself throws, that exception replaces the original one.
finally is for cleanup: releasing a lock, clearing a timer, resetting a loading flag.
let busy = true;
try {
await loadData();
} finally {
busy = false; // runs even when loadData rejects
}
Do not return from finally; it discards the try value and hides exceptions.
Filtering by error type
try {
JSON.parse(raw);
} catch (err) {
if (err instanceof SyntaxError) {
console.log("invalid JSON");
} else {
throw err; // not mine to handle
}
}
“Handle what you understand, rethrow the rest” is the habit that keeps logs useful. A catch-all that logs nothing turns failures into silence.
Since ES2022 the binding is optional when you do not need the error:
try {
doSomething();
} catch {
console.log("failed");
}
That is fine for cleanup paths; it is not fine as a substitute for real handling.
Custom error classes
Branching on message strings is brittle. Give each failure mode a type:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function requireName(user) {
if (!user.name) {
throw new ValidationError("name is required", "name");
}
}
try {
requireName({});
} catch (err) {
if (err instanceof ValidationError) {
console.log(`${err.field}: ${err.message}`); // name: name is required
}
}
Set this.name in the constructor; otherwise logs show Error for every subclass. If you target older environments, also restore the prototype chain, though modern class syntax handles it.
Keep the hierarchy shallow: one project base class (AppError) plus a few specific subclasses is easier to handle than a deep tree.
Preserving context with cause
try {
readConfig();
} catch (err) {
throw new Error("startup failed", { cause: err });
}
The outer error carries the original in err.cause, so the stack shows both. Node 18+ and all modern browsers support the option. Prefer it over stuffing the original message into a new string.
Async errors escape try/catch
// does not work
try {
setTimeout(() => {
throw new Error("async failure");
}, 100);
} catch (err) {
console.log("caught"); // never runs
}
The callback runs later, on a different call stack, after the try block has finished. How you catch depends on the async style:
Promises use .catch():
fetchUsers()
.then((users) => console.log(users))
.catch((err) => console.error("request failed:", err.message));
async / await works with try / catch (preferred):
async function load() {
try {
return await fetchUsers();
} catch (err) {
console.error("request failed:", err.message);
return [];
}
}
Every promise chain needs a .catch() at the end, or an await inside try. Otherwise you get unhandled rejections: a warning in Node today, and a process exit in modern runtimes with --unhandled-rejections=throw.
For a whole application, add global handlers as a safety net, not as the primary strategy:
process.on("unhandledRejection", (reason) => {
console.error("unhandled rejection", reason);
});
Validation beats exceptions
Exceptions are for exceptional states. Ordinary branching should be ordinary code:
// avoid: using exceptions for control flow
let value;
try {
value = obj.field;
} catch {
value = "default";
}
// prefer
const value = obj?.field ?? "default";
The same applies to parsing input: check the shape first, and reserve throw for data that violates an invariant.
Common mistakes
1. Empty catch {}
The error disappears. Log it, or write a comment explaining why ignoring it is safe.
2. Throwing strings or plain objects
No stack, no instanceof, no name. Always new Error(...).
3. Wrapping a callback in try/catch
Asynchronous errors are not catchable that way. Use promises and await.
4. return inside finally
Overrides the return value and suppresses exceptions.
5. Missing .catch() on a promise chain
Unhandled rejections are hard to trace, especially in fire-and-forget code.
6. Custom errors without name
Every log line says Error, and you cannot tell validation failures from network failures.
7. Losing the original error
throw new Error(err.message) discards the stack. Use { cause: err }.
Summary
throw new Error(msg, { cause }); never throw primitives.finallyalways runs — use it for cleanup, never forreturn.- Catch only what you can handle and rethrow the rest with
throw err. - Custom errors: extend
Error, setname, keep the hierarchy shallow. - Async failures belong to
.catch()orawaitinsidetry/catch. - Do not use exceptions for expected branches.
That completes the JavaScript basics series: bindings, coercion, control flow, functions, arrays, objects, strings, and errors.