JS error handling
JavaScript lets you throw anything and catch it as unknown, which is
exactly how error handling goes wrong: strings thrown instead of Errors,
catch (e) where e is any, and rejections that vanish. A deliberate
policy makes failures visible and diagnosable.
Method
- Throw Errors, never strings or objects. Only
Errorinstances carry a stack trace and work with tooling.throw new Error("..."), neverthrow "failed". Subclass for distinct handling:class NotFoundError extends Errorwith a setname, so callers can branch on the type (instanceof) rather than string-matching messages. - Type the catch as unknown and narrow. In TypeScript,
catch (e)givesunknown(withuseUnknownInCatchVariables): you cannot assumee.messageexists (someone threw a string). Narrow withe instanceof Errorbefore touching properties, and have a fallback for the non-Error case (see typescript-narrowing). - Preserve context with the
causechain. When catching and re-throwing at a boundary, pass the original:throw new ConfigError("bad port", { cause: err }). The chain keeps the root cause and its stack, so the final log tells the whole story instead of a bare high-level message (see rust-error-handling for the same principle typed). - Catch narrowly, at the right level. Wrap the specific operation
that can fail, not a whole function; catch where you can actually do
something (retry, default, user message). A broad
tryaround everything hides which call failed and catches errors you never meant to. Let genuinely unexpected errors propagate to a top-level handler. - Set a policy for async and unhandled failures. Every async entry
point (route, event handler, job) has one place that catches and
reports (see js-async-patterns). Register
unhandledRejection/uncaughtException(Node) andwindow.onerror/unhandledrejection(browser) as backstops that log to your error tracker (see error-tracking): they should be near-empty in a healthy app, so treat hits as bugs. - Decide errors-vs-results deliberately. For expected failure that
callers routinely handle (validation, "not found"), a result type
(
{ ok: true, value } | { ok: false, error }) or returningnull/undefinedcan be clearer than throwing; reserve exceptions for the exceptional. Whatever you choose, be consistent within a module.
Boundaries
- Error messages are user- and developer-facing text; route user-facing ones through a formatter and keep internal detail (stacks, causes) in logs, not in the UI (see error-messages).
try/catchdoes not catch errors in async callbacks that ran after the try block exited; onlyawaited rejections and synchronous throws are caught (see js-event-loop, js-async-patterns).- Swallowing errors to "keep the app running" trades a visible failure for a silent-corruption one; log before any deliberate suppression, and suppress only with a documented reason.