Skip to content

Error Handling

Scope: try/catch with async code, promise rejections, and what actually crashes a Node process. All examples were run with Node v24.

try/catch: the basics

try {
  const data = JSON.parse("not json");
} catch (err) {
  console.error("Parse failed:", err.message);
}
console.log("program continues");

try/catch works the same in async functions — and this is where beginners get tripped up:

async function loadConfig(path) {
  try {
    const text = await readFile(path, "utf8");
    return JSON.parse(text);
  } catch (err) {
    console.error("loadConfig failed:", err.message);
    return null;  // or rethrow, or exit — decide deliberately
  }
}

await inside try catches async failures. Without the try, a rejected promise becomes an unhandled rejection.

Unhandled rejections crash the process

// DON'T do this:
readFile("missing.txt", "utf8");  // rejected promise, nobody listening

Since Node 15, an unhandled promise rejection crashes the process by default. This is deliberate: silently swallowing async failures caused years of mystery bugs. The rule: every promise chain ends in either await (inside try/catch) or .catch().

Throwing your own errors

function divide(a, b) {
  if (b === 0) {
    throw new Error("can't divide by zero");
  }
  return a / b;
}

try {
  divide(1, 0);
} catch (err) {
  console.error(err.message);  // can't divide by zero
}

throw works in async functions too — the rejection carries the thrown error to the caller's catch.

The error-first callback legacy

Old Node APIs signal failure by passing the error as the first callback argument:

import fs from "node:fs";
fs.readFile("x.txt", "utf8", (err, data) => {
  if (err) {
    console.error("failed:", err.message);
    return;
  }
  console.log(data);
});

You don't need to write this style — use node:fs/promises with await instead. But you'll read it in old docs, so recognize the shape: check err first, always.

Failing loudly on purpose

if (!config.apiKey) {
  throw new Error("config.apiKey is required");
}

Validate early and throw with a clear message. A crash with "config.apiKey is required" at startup beats a mysterious 401 Unauthorized three hours later.

For CLIs, exit with a non-zero code after logging:

console.error("error: missing input file");
process.exit(1);

The decision table

Situation Do this
Sync code that can fail try/catch
await that can fail try/catch around it
Fire-and-forget promise End the chain with .catch()
Impossible situation / bad config throw new Error("clear message")
Old callback API Check err first, or use the /promises version

Check

  • [ ] You can explain why unhandled rejections crash the process
  • [ ] You know await inside try catches async failures
  • [ ] You can recognize the error-first callback pattern

Next: Troubleshooting.