Errors#
JavaScript throws exceptions with throw and catches them
with try / catch / finally. The thrown value is
usually an Error or subclass; the operator who throws a
string or a number is making the catch side harder. Async error
flow is the same; await on a rejected promise throws inside
the surrounding async function.
For async / await mechanics, see
Concurrency.
Throwing#
throw raises. The thrown value can be anything but is almost
always an Error so the catcher gets a name, message, and
stack.
function parsePort(s) {
const n = Number(s);
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
throw new TypeError(`bad port: ${s}`);
}
return n;
}
The standard Error subclasses are TypeError,
RangeError, SyntaxError, ReferenceError, URIError,
EvalError. The operator throws the closest fit; Error
itself when nothing else applies.
Try / catch#
try runs a block; catch runs if it throws; finally
runs either way after both.
try {
const port = parsePort(input);
await connect(port);
} catch (e) {
console.error("failed:", e.message);
return null;
} finally {
cleanup();
}
The catch binding is optional (catch { ... }) when the
operator does not need the value.
try { JSON.parse(input); }
catch { return null; } // optional binding (ES2019+)
Custom errors#
Subclass Error and set name so the catch side can
discriminate.
class HTTPError extends Error {
constructor(status, body) {
super(`HTTP ${status}`);
this.name = "HTTPError";
this.status = status;
this.body = body;
}
}
try { await call(); }
catch (e) {
if (e instanceof HTTPError && e.status === 401) {
refresh();
} else {
throw e; // re-raise anything else
}
}
Re-raising preserves the original stack; wrapping with a
cause is the modern way to add context without losing the
original.
try { await fetchUser(id); }
catch (e) {
throw new Error(`load failed for ${id}`, {cause: e});
}
The catcher reads e.cause to get the wrapped original.
Async errors#
A rejected promise that nothing handles becomes an
unhandledRejection. In Node, that crashes the process by
default (configurable); in browsers, it logs to the console.
// explicit handling
try {
const r = await fetch(url);
if (!r.ok) throw new HTTPError(r.status);
} catch (e) {
log.error(e);
}
// promise-level handling
fetch(url).catch(e => log.error(e));
// global trap (Node)
process.on("unhandledRejection", e => log.fatal(e));
Promise.all rejects on the first failure; Promise.allSettled
returns the per-promise result with status and value
or reason.
const results = await Promise.allSettled(urls.map(fetch));
const errors = results.filter(r => r.status === "rejected");
AggregateError carries multiple errors at once
(Promise.any produces one when all input promises reject).
Stack traces#
error.stack is the formatted trace. Error.captureStackTrace
(V8 / Node) lets a constructor skip frames so the trace points at
the caller, not the helper.
class ValidationError extends Error {
constructor(msg) {
super(msg);
Error.captureStackTrace(this, ValidationError);
}
}
In production, source maps (--enable-source-maps in Node)
translate minified frames back to source lines.
Assertions#
Node’s assert (and assert/strict) raises AssertionError
when a check fails. Use for invariants that are programmer
errors, not user errors.
import assert from "node:assert/strict";
function send(host, port) {
assert.equal(typeof host, "string");
assert.equal(typeof port, "number");
// …
}
References#
Control flow for the surrounding jump surface (
throw,return,break).Concurrency for promise rejection and async-error propagation.
Testing for
assertfamily inside specs.