skip to content

The Silent Promise That Will Kill Your Node.js Server

/ 2 min read

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 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 — 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 promises
doSomethingAsync().catch(() => {});
// Or be explicit about ignoring the result
void doSomethingAsync().catch(err => {
console.error('Background task failed:', err);
});

The Lesson

try-catch and async/await are not magic. They follow specific rules:

  1. try-catch only catches synchronous throws and awaited rejections
  2. A Promise without await or .catch() is a ticking time bomb
  3. “Fire and forget” still needs a .catch() — you’re forgetting the result, not the error

Your server will thank you.