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#