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:
saveDataInBackground(data);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) { const stream = await callAI(request);
// Fire and forget - don't wait for this saveMessageToDatabase(stream);
return stream;}Looks fine. You even added error handling inside saveMessageToDatabase:
async function saveMessageToDatabase(stream) { try { const data = await collectFromStream(stream); await db.save(data); } catch (error) { console.error('Failed to save:', error); }}Ship it. What could go wrong?
The Kill Shot
Three hours later, the AI provider has a hiccup. The stream connection drops mid-transfer. collectFromStream throws an error.
But that’s ok, you’ve got a “try-catch.”
But your server is already 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 uselesstry { 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 — the internal try-catch already logs the error. You just need to mark the Promise as “handled.”
The Pattern
For any fire-and-forget async operation:
// Always attach .catch() to floating promisesdoSomethingAsync().catch(() => {});
// Or be explicit about ignoring the resultvoid 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.