How do you handle errors in asynchronous Node.js code, and what is an unhandled promise rejection?
Use try/catch around await, .catch() on promise chains, and the error-first argument in callbacks. An unhandled rejection is a promise that fails with no handler attached; since Node 15 it crashes the process by default, so every promise needs a catch somewhere.
Explain it simply
When you place a Swiggy order, many things can fail later: the restaurant may cancel, the payment may bounce. If nobody is assigned to handle those failures, the order just breaks silently and you never hear back. In async code, errors also happen later, after the function that started the work has already moved on. You need to say in advance who will handle the failure.
A plain try/catch only catches errors thrown while it is running. It cannot catch an error that happens later inside a callback, unless you are using await, which pauses the function so the error comes back into the try block.
Example
const fs = require("node:fs/promises");
// 1. async/await with try/catch
async function loadConfig() {
try {
const text = await fs.readFile("./config.json", "utf8");
return JSON.parse(text);
} catch (err) {
console.error("Could not load config:", err.message);
return {}; // fall back to defaults
}
}
// 2. Promise chain with .catch()
fs.readFile("./config.json", "utf8")
.then(JSON.parse)
.catch((err) => console.error("Failed:", err.message));
// 3. Old callback style: error is the first argument
require("node:fs").readFile("./config.json", "utf8", (err, text) => {
if (err) return console.error(err.message);
console.log(text);
});This is the bug that causes an unhandled rejection:
async function saveOrder() {
throw new Error("DB down");
}
saveOrder(); // no await, no .catch() -> unhandled rejection, process exitsHow it works
- A rejected promise looks for a
.catch()or atry/catcharound anawait. If none is attached by the time the microtask queue is processed, Node emits theunhandledRejectionevent. - Since Node 15, the default mode is
--unhandled-rejections=throw: the rejection is raised as an uncaught exception and the process exits with a non-zero code. process.on("uncaughtException")andprocess.on("unhandledRejection")let you log the error before shutting down. They are a last safety net, not a normal way to handle errors.
Real-world use
In an API, wrap each request so failures become a proper HTTP error response (for example a central Express error handler). For background jobs, catch errors per job so one failed SMS does not stop the whole batch. Use a process manager such as PM2, systemd or Kubernetes to restart the app if it does crash.
Common mistakes
- Calling an async function without
awaitor.catch()("fire and forget"). - Using
array.forEach(async ...); the errors and the completion are not awaited. Usefor...ofwithawait, orPromise.all. - Wrapping a callback-based call in
try/catchand expecting it to catch the async error. - Using
uncaughtExceptionto keep the server running. After an unknown error the app state may be broken; log it and restart.
What to say in the interview
"For async code I use try/catch around await, .catch() on promise chains, and check the error argument in callback APIs. An unhandled promise rejection is a promise that rejects with nothing handling it. In modern Node that crashes the process by default, which is good because silent failures are worse. I make sure every promise is awaited or has a catch, I use Promise.allSettled when some failures are acceptable, and I add unhandledRejection and uncaughtException listeners only to log and shut down cleanly before a process manager restarts the app."
Follow-up questions
- Why does
try/catchnot catch an error thrown inside asetTimeoutcallback? - What is the difference between
Promise.allandPromise.allSettledfor error handling? - Should you keep the process running after an
uncaughtException?
Go deeper
Read these once the basics above feel easy.
Custom error classes (class NotFoundError extends Error) let a central handler map errors to status codes. Since ES2022, new Error("Payment failed", { cause: err }) keeps the original error attached, which makes logs much easier to follow across layers. For graceful shutdown, stop accepting new connections with server.close(), finish in-flight requests, close database pools, and then exit.