Promise result is unhandled
| Vulnerability potential | Low |
| DDoS potential | Low |
The Promise has no .then/.catch and is not awaited; rejections propagate as uncaught and the value is discarded
Impact
A Promise that is neither awaited nor given a .then/.catch is a
fire-and-forget operation whose result and errors are discarded. Two distinct
problems follow. First, the resolved value is lost: if the call did real work
(a fetch, a DB write, a computation) the program continues without it, often
racing ahead of an operation that has not finished — a classic source of “it
works locally but the data is sometimes missing” bugs. Second, and more serious,
a rejection has nowhere to go and becomes an unhandled rejection. In Node.js
the default behaviour since v15 is to print the error and terminate the process
with a non-zero exit code, so a single unhandled rejection on a request path can
crash the server. In browsers it fires a global unhandledrejection event and
logs to the console, leaving the operation half-done and the failure invisible
to the user.
Vulnerability potential
The security impact is mostly availability and error-masking rather than injection or memory safety.
- Denial of service. Under Node’s default
--unhandled-rejections=throw, any code path whose Promise can reject without a handler lets an attacker crash the process by steering input down that path (a malformed request, a failing downstream call). On an unsupervised process this is an outage; even with a supervisor it is a restart-amplified resource drain — hence the Low DDoS rating. - Silenced security errors. If the discarded rejection was a failed authorization, signature check or validation, the program proceeds as if it succeeded, so a security-relevant failure passes unnoticed.
The defect does not itself grant code execution or data access, so the vulnerability rating stays Low; the realistic harm is crashes and masked failures.
Technical details
A Promise tracks whether it has a rejection handler. If it settles to rejected
and the microtask queue drains without any .then(onRejected)/.catch/await
having been attached, the host raises an “unhandled rejection”. Attaching a
handler later (after the tick) clears a previously reported rejection via the
rejectionhandled event, which is why the warning is timing-sensitive.
Node.js
Since Node 15 the default mode is --unhandled-rejections=throw: the rejection
is thrown from the top level and, if not caught by a process.on
('unhandledRejection') handler, terminates the process. Older modes (warn,
none) only logged. A leftover global handler that swallows rejections trades a
crash for silent data loss, so it is not a real fix.
Browsers
The window/globalThis unhandledrejection event fires and the error is
logged to the console; the page keeps running but the operation is incomplete.
await vs floating
Inside an async function, await p routes a rejection into the surrounding
try/catch (or rejects the function’s own Promise). Calling p without
await or a .catch leaves it “floating”. void p documents intent but does
not add a handler — it still rejects unhandled.
Catching the issue
Linters and types
ESLint’s no-floating-promises (in @typescript-eslint, type-aware) flags any
Promise that is not awaited, returned, or given a .catch, and is the single
most effective control; pair it with no-misused-promises (Promises passed
where a void callback is expected) and require-await. TypeScript itself does
not catch floating Promises, but the type-aware lint rule needs its type info.
Runtime nets
Register process.on('unhandledRejection', ...) in Node and a
window.addEventListener('unhandledrejection', ...) in the browser to log and
report (to Sentry etc.) — as observability, not as a substitute for handling
each Promise. Keep Node in the default throw mode so failures are loud.
Review
Every async call should be awaited, returned, or explicitly handled with
.catch. A bare doAsync(); on its own line is a defect.
How to reproduce
Run under Node (node file.js) and observe the process exiting non-zero with
“Unhandled promise rejection”; the awaited version handles it cleanly.
function risky() {
return Promise.reject(new Error("boom"));
}
// floating: rejection is unhandled -> Node crashes the process
risky();
// fixed: await inside try/catch (or attach .catch)
async function main() {
try {
await risky();
} catch (err) {
console.error("handled:", err.message);
}
}
main();