Quick answer
A promise rejected (or an async function threw) and nothing handled it — no try/catch, no .catch(). In Node.js 15+ this crashes the process. Fix it by wrapping awaited calls in try/catch, attaching .catch(), and never firing async work without handling its errors. The usual culprit is a forgotten await.
The exact error string
// older Node:
(node:123) UnhandledPromiseRejectionWarning: Error: boom
// Node 15+ (default — the process exits):
node:internal/process/promises:288
triggerUncaughtException(err, true /* fromPromise */);
Error: boom
... (exit code 1)
How to find the rejecting promise
The warning prints the reason (the original error) and a stack. When that isn't enough, force more detail and log the rejection centrally while you hunt:
node --trace-warnings app.js # full stack for the warning
node --unhandled-rejections=strict app.js # throw immediately at the source
// temporary global logger to capture the reason + where it came from:
process.on("unhandledRejection", (reason) => {
console.error("UNHANDLED:", reason);
});
The reason is the real error; trace back to the async call that produced it and that has no await/.catch around it.
Fix 1: try/catch in async functions
async function load() {
try {
const res = await fetch(url);
return await res.json();
} catch (err) {
console.error("load failed:", err);
return null; // handle / rethrow deliberately
}
}
Fix 2: forgotten await / fire-and-forget
save(data); // ❌ if save() rejects, it's unhandled
await save(data); // ✅ inside a try/catch
save(data).catch(err => log(err));// ✅ or attach a .catch()
Fix 3: .catch() on a promise chain
fetch(url)
.then(res => res.json())
.then(use)
.catch(err => console.error(err)); // always terminate the chain
Fix 4: Express routes
app.get("/u/:id", async (req, res, next) => {
try {
res.json(await getUser(req.params.id));
} catch (err) {
next(err); // hand to Express error middleware
}
});
Worked example: a forgotten await on an API call
The classic real-world trigger: an async helper is called without await (or its returned promise is dropped), so a network/parse rejection escapes and crashes the app later, far from the cause:
async function syncUser(id) {
const res = await fetch(`/api/user/${id}`);
return res.json(); // rejects if the body isn't JSON
}
function onLogin(id) {
syncUser(id); // ❌ promise dropped — rejection is unhandled
}
// ✅ handle it where you start the work
async function onLogin(id) {
try { await syncUser(id); }
catch (err) { showError(err); }
}
If the rejection is a JSON parse error, the body probably wasn't JSON — inspect it with the JSON Formatter and check the HTTP status (a 4xx/5xx returns an error page, not your object).
Unhandled rejections in React
// ❌ an async effect callback returns a promise React won't catch
useEffect(async () => {
const data = await fetchUser(); // a rejection here is unhandled
setUser(data);
}, []);
// ✅ define an async fn inside and catch it
useEffect(() => {
(async () => {
try { setUser(await fetchUser()); }
catch (err) { setError(err); }
})();
}, []);
Common causes of this message
| Cause | Where | Fix |
|---|---|---|
forgotten await / fire-and-forget | any async call | await in try/catch or .catch() |
.then() with no .catch() | promise chains | terminate with .catch() |
| async route handler throws | Express (pre-v5) | try/catch + next(err) |
async useEffect callback | React | inner async fn + catch |
| async event listener throws | emitters/DOM | wrap the listener body in try/catch |
Promise.all member rejects | concurrent work | catch the combined promise |
Last resort: a process-level safety net
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
// log, flush, then let the process exit / restart under a supervisor
});
Debugging checklist
- ✓ Read the reason; use
--trace-warningsfor a full stack - ✓
awaitinsidetry/catch, or attach.catch() - ✓ Forgot an
await? Fire-and-forget rejections escape - ✓ Always terminate
.then()chains with.catch() - ✓ Async Express/event handler?
try/catch+next(err) - ✓ React? Don't make
useEffectasync — use an inner async fn + catch - ✓ Node 15+ crashes on unhandled rejections — the global handler is only for logging
Frequently Asked Questions
How do I find which promise rejected?
Read the reason and stack the warning prints. If the stack is unhelpful, run node with --trace-warnings (or --unhandled-rejections=strict) for a full trace, and add a process.on('unhandledRejection', r => console.error(r)) handler temporarily to log the rejection value. The reason is the original error that was never caught.
Does it really crash the process in modern Node?
Yes. Since Node.js 15 the default unhandled-rejections mode is throw, so an unhandled rejection terminates the process with a non-zero exit code. Earlier versions only printed UnhandledPromiseRejectionWarning. Don't rely on the old warning-only behaviour.
What's the difference between unhandledRejection and uncaughtException?
uncaughtException is a synchronous throw that no try/catch caught; unhandledRejection is a rejected promise with no .catch/await. Async errors become rejections, so in promise-heavy code unhandledRejection is the one you hit. Both are last-resort process events, not a substitute for handling errors locally.
Why can't I make a React useEffect callback async?
An async function returns a promise, but useEffect expects either nothing or a cleanup function — so the rejection is unhandled and the cleanup contract breaks. Define an async function inside the effect and call it with a .catch, or wrap the body in try/catch.
Does returning a promise without awaiting cause it?
If you return the promise to a caller that awaits or catches it, you're fine. The problem is fire-and-forget: calling an async function and ignoring the returned promise. Either await it inside try/catch, attach .catch(), or explicitly mark it ignored with void only when you truly don't care (rare).
Will a global handler make my app safe?
No. process.on('unhandledRejection') is a safety net for logging what slipped through; it doesn't make the code correct and shouldn't be used to keep a broken process alive. Handle rejections where they happen, and use the handler to catch the rare miss, then exit/restart under a supervisor.
Debugging async API calls?
See practical fetch, error-handling, and inspection patterns in our API testing guide.