How a service fails decides whether an incident is a logged warning or a crashed, corrupted process.
Two categories of error need different treatment:
- Operational errors are expected failures of a correct program: file not found, connection refused, invalid user input, a timeout. Handle them — retry, return a 4xx, fall back to defaults.
- Programmer errors are bugs:
undefined is not a function, a wrong argument type. You can't meaningfully recover; log, crash, and let a supervisor restart the process.
Node's errors carry a stable `code` property (ENOENT, ECONNREFUSED, ERR_INVALID_ARG_TYPE). Branch on err.code, never on message text, which can change between versions. Do the same in your own code: subclass Error, set name and a code, and add context fields.
When you catch and rethrow, keep the original with `cause`: throw new Error("loading user 42 failed", { cause: err }). Logs and util.inspect then show both stacks.
Async rules:
try/catchonly catches a rejection if youawaitthe promise inside thetry.- A promise rejection nobody handles triggers
unhandledRejection; since Node 15 the default is to crash with exit code 1. - A throw that escapes everything triggers
uncaughtExceptionand also crashes.
You can listen for both on process, but use them only to log and exit (process.exit(1) after flushing logs). After an uncaught exception, the process may be in an inconsistent state — half-written data, held locks — so continuing is dangerous. Run under something that restarts you (systemd, Docker restart policy, Kubernetes).
Finally, throw Error objects, not strings: only errors have stack traces.
Example
import { readFile } from "node:fs/promises";
class NotFoundError extends Error {
name = "NotFoundError";
code = "E_NOT_FOUND";
constructor(what, options) { super(`${what} not found`, options); }
}
async function loadUser(id) {
try {
return JSON.parse(await readFile(`users/${id}.json`, "utf8"));
} catch (err) {
if (err.code === "ENOENT") throw new NotFoundError(`user ${id}`, { cause: err });
throw new Error(`loading user ${id} failed`, { cause: err });
}
}
await loadUser(42).catch((err) => {
console.log(err.code, "-", err.message, "| cause:", err.cause.code);
// E_NOT_FOUND - user 42 not found | cause: ENOENT
});
process.on("unhandledRejection", (reason) => {
console.error("unhandled rejection", reason);
process.exit(1); // log, then crash; let the supervisor restart us
});Remember
- Handle operational errors; crash and restart on programmer errors
- Branch on
err.code, wrap with{ cause }to keep context - Unhandled rejections crash Node by default
- Use uncaught handlers only to log and exit
Watch out for
- Catching
uncaughtExceptionand continuing to serve requests from a corrupted state. try { promise } catchwithoutawait, so the rejection escapes.