Throwing Errors
When something goes wrong in your code — invalid input, a missing resource, a broken invariant — you need a way to stop normal execution and signal that failure to whoever called you. JavaScript does this with the throw statement, which raises an exception that travels up the call stack until something catches it. Throwing well-formed errors with clear messages is one of the highest-leverage habits you can build: it turns cryptic, hard-to-debug failures into precise, actionable signals.
The throw statement
throw interrupts the current function immediately. Any expression that follows it becomes the thrown value, and control jumps to the nearest enclosing catch block (or terminates the program if there is none).
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
divide(10, 0);
Output:
Uncaught Error: Cannot divide by zero
at divide (<anonymous>:3:11)
at <anonymous>:7:1
Everything after a reached throw in the same scope is unreachable — the function does not return normally.
Throw Error objects, not primitives
You can throw any value — a string, a number, an object, even undefined — but you almost always should not. Throwing an Error object (or a subclass) gives you a name, a human-readable message, and a captured stack trace for free. Primitives carry none of that context.
// Avoid: no stack trace, no type information
throw "Something failed";
throw 404;
// Prefer: rich, debuggable, and works with instanceof
throw new Error("Something failed");
Warning: Code that catches errors often assumes it received an
Errorand readserr.messageorerr.stack. Throwing a string or number breaks those assumptions and silently producesundefinedproperties downstream.
The difference shows up immediately when you inspect what was caught:
| Thrown value | err.message | err.stack | err instanceof Error |
|---|---|---|---|
new Error("boom") | "boom" | full trace | true |
"boom" | undefined | undefined | false |
404 | undefined | undefined | false |
The Error object
The built-in Error constructor accepts a message string and an optional options object. The instance it produces exposes a small but important set of properties.
| Property | Description |
|---|---|
message | The human-readable string passed to the constructor. |
name | The error category, e.g. "Error" or "TypeError". Used in toString(). |
stack | A non-standard but universally supported string describing where the error was created. |
cause | The underlying error or value that triggered this one (ES2022, options bag). |
const err = new Error("Database connection failed");
console.log(err.name);
console.log(err.message);
console.log(err.toString());
Output:
Error
Database connection failed
Error: Database connection failed
Built-in subclasses like TypeError, RangeError, and SyntaxError set name automatically, which makes your throw statements self-describing:
function setVolume(level) {
if (typeof level !== "number") {
throw new TypeError(`Expected a number, received ${typeof level}`);
}
if (level < 0 || level > 100) {
throw new RangeError("Volume must be between 0 and 100");
}
return level;
}
Error cause
The cause option (ES2022) lets you attach the original error when you wrap one failure in another. This preserves the full chain instead of discarding low-level detail.
async function loadProfile(userId) {
try {
return await fetch(`/api/users/${userId}`).then((r) => r.json());
} catch (networkError) {
throw new Error(`Failed to load profile for ${userId}`, {
cause: networkError,
});
}
}
The high-level message stays readable, while err.cause retains the original network error for logging and debugging.
Writing helpful messages
A good error message states what went wrong and, ideally, gives enough context to fix it. Include the offending value, but never embed secrets such as passwords or tokens.
// Vague — forces a debugging session
throw new Error("Invalid input");
// Specific — diagnosable at a glance
throw new Error(`Invalid email "${email}": missing "@" symbol`);
Rethrowing
Inside a catch block you frequently inspect an error, decide you cannot handle it, and pass it along. Rethrowing the same object preserves the original stack trace; creating a new one (without cause) loses it.
function parseConfig(raw) {
try {
return JSON.parse(raw);
} catch (err) {
if (err instanceof SyntaxError) {
// Handle: this layer knows how to recover
return {};
}
throw err; // Anything else is not ours to handle — pass it up
}
}
When you want to add context while still preserving the original, rethrow a new error with cause rather than swallowing the old one.
Best Practices
- Always throw
Errorinstances (or subclasses), never strings, numbers, or plain objects. - Pick the most specific built-in type —
TypeError,RangeError— so callers can branch oninstanceof. - Write messages that name the failure and include the relevant value, but never leak secrets.
- Use the
causeoption to wrap low-level errors without losing their stack trace. - Rethrow the original error object (
throw err) when you only inspected it; don’t recreate it and discard the stack. - Never throw inside a
finallyblock — it overrides any error already in flight and hides the real cause. - Fail fast: validate inputs and throw at the top of a function instead of letting bad data propagate.