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
awaitinsidetrycatches async failures - [ ] You can recognize the error-first callback pattern
Next: Troubleshooting.