Safeguard
Developer Tools•

Why does a forgotten await matter in Node, and how do you lint it?

A promise nobody handles can skip a security check or crash the process. How Node treats unhandled rejections and how no-floating-promises catches them.

Safeguard Security Team
3 min read

A forgotten await lets code continue before an operation finishes, and a rejection nobody handles can crash the process. In Node.js the default for an unhandled rejection is to raise it as an uncaught exception. The typescript-eslint rule no-floating-promises finds these statements before they ship.

The security shape of the bug

Consider an authorization helper that returns a promise. If a route calls it without await, the handler carries on and serves the data while the check runs in the background. If the check later rejects, nothing stops the response that already went out. The tests pass, because the happy path still works.

The same pattern appears with audit log writes, token revocation and cache invalidation. In each, the code looks right and does nothing useful.

What Node does with an unhandled rejection

The process docs say the unhandledRejection event fires when a promise is rejected and no handler is attached within a turn of the event loop. If that event is emitted but nobody handles it, it is raised as an uncaught exception. The CLI docs name throw as the default mode for --unhandled-rejections. An uncaught exception prints a stack trace and exits with code 1 unless you added a handler. See the modes compared for the other options.

What the lint rule checks

The typescript-eslint docs define a floating promise as one created without any code to handle errors it might throw. The rule treats a promise as handled when you:

  • call .then() with two arguments,
  • call .catch() with one argument,
  • use await, or
  • use return.

It also accepts the void operator by default through ignoreVoid, but the docs warn that voiding does not handle the promise, so its rejection is still unhandled. It flags arrays of promises too and suggests Promise.all, Promise.allSettled, Promise.any or Promise.race. It needs type information and is part of recommended-type-checked.

Steps to adopt it

  1. Enable typed linting using the preset guide.
  2. Run the rule and read each finding. Most are real.
  3. Fix with await where order matters, or return the promise.
  4. For deliberate fire and forget work, add a .catch() that logs, instead of void alone.
  5. Enable no-misused-promises as well. The docs say misuse in places such as if conditions is covered by that separate rule, not this one.
  6. Make lint a required check.

Things the rule cannot see

It will not know about a function that returns a promise through an untyped any, which is another reason to keep noImplicitAny on. It also cannot tell you the right behavior on failure, such as deny on error for authorization.

Quick check

Does void make a promise safe?

No. The rule allows it by default, but the docs say a voided promise's rejection remains unhandled.

Does the rule work without type information?

No. It requires type information and is enabled by recommended-type-checked.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.