You've got a production server. It's been running fine for hours. Then suddenly it crashes. No warning, no graceful degradation. Just dead.
The culprit? A single line of code that looks completely innocent:
saveMessageToDatabase(stream);
The Setup
You're building a chat API. When a user sends a message, you want to stream the response back immediately while saving it to the database in the background. Classic fire-and-forget pattern:
async function handleChat(request) {
try {
const stream = await callAI(request);
// Fire and forget - don't wait for this
saveMessageToDatabase(stream);
return stream;
} catch (error) {
console.error('Failed:', error);
}
}
Looks fine. What could go wrong?
The Kill Shot
Three hours later, the AI provider has a hiccup. The stream connection drops mid-transfer. saveMessageToDatabase throws an error.
Your server is dead.
"PM2/Docker/Kubernetes will restart it!" Sure — and you've still lost every in-flight request, any unsaved state, and your users just saw an error. Prevention beats recovery.
Why try-catch Didn't Save You
try-catch only catches errors from awaited promises:
// ❌ This try-catch is useless
try {
saveMessageToDatabase(stream); // Returns immediately
} catch (error) {
// Never reached
}
Without await, the function returns a Promise and exits the try block successfully. The actual error happens later, asynchronously, after the try-catch is long gone.
When that Promise eventually rejects with no handler attached, Node.js sees an "unhandled rejection" and, depending on your Node version, terminates the process.
The Fix
One character. Well, a few:
saveMessageToDatabase(stream).catch(() => {});
That's it. By attaching .catch(), you tell Node "I know this might fail, and I'm handling it." The callback can be empty if you're logging inside the function. You just need to mark the Promise as "handled."
The Pattern
For any fire-and-forget async operation:
// Always attach .catch() to floating promises
doSomethingAsync().catch(() => {});
// Or log the error if you want visibility
doSomethingAsync().catch(err => {
console.error('Background task failed:', err);
});
The Lesson
try-catch and async/await are not magic. They follow specific rules:
-
try-catchonly catches synchronous throws and awaited rejections - A Promise without
awaitor.catch()is a ticking time bomb - "Fire and forget" still needs a
.catch()— you're forgetting the result, not the error
Your server will thank you.
Top comments (0)