← Toutes les actualités
devops5 min de lecture

Unhandled promise rejections are silently crashing production apps

Par Seaworthy · 11 juin 2026

Error text displayed on a computer screen

Photo by David Pupăză on Unsplash

AI assistants write async code the way most online tutorials do: straight down the happy path, no detours. You ask for a function that fetches a user record, and you get something that works perfectly in your terminal and silently explodes in production.

This is not a subtle bug. It is a structural problem with how large language models learn to write code.

What the training data looks like

Most code examples on the internet were written to demonstrate a concept, not to survive production. Stack Overflow answers show the minimal path to "it works." Blog posts skip error branches to keep the explanation short. LLMs absorb this corpus and reproduce the same optimism at scale.

The result is async code that handles success and ignores failure.

// What AI tools tend to produce
async function getUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  return data;
}

This function fails silently if the network drops. It returns garbage if the server responds with a non-JSON error body. If fetch itself throws, the promise rejects and, depending on your Node version and event listener setup, that rejection either crashes your process or vanishes without a trace.

Neither outcome is acceptable. Neither is obvious in a demo environment, where the upstream is always up and the response is always valid JSON.

How Node actually handles this

Node's behavior around unhandled rejections has shifted across major versions, which makes this problem worse to reason about. Before Node 15, an unhandled rejection emitted a deprecation warning to stderr and continued execution. Starting with Node 15, an unhandled rejection crashes the process with exit code 1 by default.

So depending on what version your production environment runs, the same AI-generated code either kills your server or silently drops the error. Both behaviors are bad, just in different directions.

If you are running a platform that auto-restarts crashed processes, you might not notice the problem until you look closely. The process dies, restarts, and handles the next request fine. Your users see occasional failures. Nothing useful ends up in your logs because the rejection handler never ran.

// Fixed version
async function getUserData(userId) {
  let response;
  try {
    response = await fetch(`/api/users/${userId}`);
  } catch (err) {
    // Network error, DNS failure, timeout
    throw new Error(`Failed to reach user service: ${err.message}`);
  }

  if (!response.ok) {
    throw new Error(`User service returned ${response.status} for userId ${userId}`);
  }

  let data;
  try {
    data = await response.json();
  } catch (err) {
    throw new Error(`User service returned non-JSON body: ${err.message}`);
  }

  return data;
}

Yes, this is more code. That is the point. The AI version is shorter because it omits the parts that matter in production.

The .catch() trap

There is a related pattern worth knowing. AI tools sometimes add a .catch() call as a gesture toward error handling, but attach it in a way that drops the error on the floor:

// Looks handled, isn't
fetchUserData(id).catch(() => {});

// Logs, but doesn't propagate
fetchUserData(id).catch(console.log);

The first version silently swallows every failure. The second logs it to stdout but does not re-throw, so any caller that awaits this function thinks it succeeded. I have seen production codebases where developers added .catch() calls everywhere to silence a linter warning, without understanding that they had just made every error invisible.

What you can actually do about it

A global unhandledRejection listener will not fix poorly written async code, but it will at least tell you the code is poorly written:

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
  process.exit(1);
});

Put this near the top of your entry point. When a rejection slips through, you will get a log line and a clean exit instead of silent failure.

For catching these at development time, the ESLint rule @typescript-eslint/no-floating-promises flags promises that are not awaited or caught. It is disabled in most default configs, which is a real gap given how much async code AI tools now produce.

{
  "rules": {
    "@typescript-eslint/no-floating-promises": "error"
  }
}

Turn it on. Accept that it will produce a lot of red lines. The AI will offer to fix them, which is fine, but read each fix before you accept it.

The actual problem

I do not think this is a failure of AI tools in any interesting technical sense. It is pattern matching on a corpus that rewards brevity and penalizes verbosity. Error handling is verbose. Training data disproportionately shows the happy path. The output reflects the input.

What this means in practice: every async function that comes out of an AI assistant should be treated as a first draft with the error branches missing. Not because the model cannot write error handling, but because the signal it was trained on pointed toward concise, demo-ready code.

Before you ship any AI-generated async function, ask yourself what happens when the network is down and what happens when the upstream returns a status you did not expect. The AI almost certainly did not ask those questions.

Seaworthy flags unhandled promise rejections and missing error branches as part of its resilience checks.

Cet article a été généré par IA et résume des sources publiquement disponibles.

Par les créateurs de Seaworthy

Seaworthy analyse votre dépôt pour détecter les problèmes abordés dans des articles comme celui-ci.

Failles de sécurité, secrets exposés et mauvaises configurations détectés avant le déploiement. Gratuit, aucun compte requis.

$ npx seaworthycode