The throw Statement
throw is JavaScript's way to signal that something is wrong. It immediately stops the current execution path and starts looking for a matching catch block up the call stack — unwinding stack frames, running finally blocks along the way, until something handles it or the runtime gives up. The expression you throw can be any value, but in practice you should always throw an Error.
The syntax
throw takes a single expression. It cannot take an empty expression, and it does not accept a line break between throw and the value (a long-standing automatic-semicolon-insertion footgun).
throw new Error("invalid input");
// Don't do this — ASI inserts a semicolon after `throw` and you get a SyntaxError:
// throw
// new Error("oops");You can throw any value
The language does not restrict what you can throw. Strings, numbers, objects, even undefined — they all propagate the same way. The catch block receives whatever was thrown.
try {
throw "bad input";
} catch (err) {
console.log(typeof err, err); // "string" "bad input"
}
try {
throw { code: 404, message: "not found" };
} catch (err) {
console.log(err.code); // 404
}Creating Error instances
new Error(message) is the baseline. You can also pass { cause } as a second argument to chain a lower-level error, and the built-in subtypes (TypeError, RangeError) accept the same shape.
function parseConfig(text) {
try {
return JSON.parse(text);
} catch (cause) {
throw new Error("config is not valid JSON", { cause });
}
}
try {
parseConfig("not-json");
} catch (err) {
console.log(err.message); // "config is not valid JSON"
console.log(err.cause); // the original SyntaxError
}throw inside conditions
Guard clauses are one of the most common shapes of throw. They keep the happy path flat and put failure cases up top.
function withdraw(account, amount) {
if (typeof amount !== "number" || Number.isNaN(amount)) {
throw new TypeError("amount must be a number");
}
if (amount <= 0) {
throw new RangeError("amount must be positive");
}
if (amount > account.balance) {
throw new Error("insufficient funds");
}
account.balance -= amount;
return account.balance;
}throw is an expression-position killer
Because throw is a statement, you cannot use it in expression positions — not in a || throw, not as a default arrow body, not directly inside a ternary. A common workaround is a small helper:
// Helper that "throws as an expression"
const fail = (msg) => { throw new Error(msg); };
const user = users.find((u) => u.id === id) ?? fail("user not found");
const port = Number(process.env.PORT) || fail("PORT is required");A TC39 proposal to make throw usable as an expression has been discussed for years, but is not in the language yet. The helper above is the idiomatic stand-in.
Re-throwing
A catch block can inspect an error and either handle it or re-throw it. Re-throwing keeps the original stack trace, so the upstream caller still sees where it came from.
try {
await loadData();
} catch (err) {
if (err instanceof NetworkError) {
showOfflineBanner();
return;
}
throw err; // anything else propagates with its original stack
}If you want to add context, throw a new error with the original as cause — that keeps both stacks linked.
try {
await readFile(path);
} catch (cause) {
throw new Error(`failed to read ${path}`, { cause });
}throw is fast — but not free
Capturing a stack trace involves walking the call stack and stringifying frames. For exceptional cases that is negligible. For "expected" failure modes inside a hot loop, throwing/catching can slow code down compared to a result-style return.
Use
throwfor exceptional paths — programmer errors, invariants, conditions you do not normally expect.Use return values (or a
[error, value]tuple) for expected failures inside hot paths — validation, lookups that often miss.Never use
throwfor control flow inside tight loops. It works, but it is slower and harder to read than areturn.
throw inside async functions
Throwing inside an async function does not produce a synchronous exception at the call site — it rejects the promise the function returns. From the caller's point of view there is no difference between throw new Error(...) and return Promise.reject(new Error(...)).
async function risky() {
throw new Error("nope");
}
risky().catch((err) => console.log(err.message)); // "nope"