Repository navigation
Promise rejection in timeout (versus module-level) treated as unhandled by debugger #53732
Description
Activity
- addedpromisesIssues and PRs related to ECMAScript promises.Issues and PRs related to ECMAScript promises.debuggerIssues and PRs related to the Node.js command-line debugger.Issues and PRs related to the Node.js command-line debugger.
on Jul 4, 2024 It's reproducible via the VSCode debugger.

The debugger treats it as both a caught exception, and an uncaught exception.
It appears to be a debugger specific issue, as it does not emit an
uncaughtException(orunhandledRejection) event on the process:process.on("uncaughtException", () => console.log("uncaughtException")) // This is never called process.on("unhandledRejection", () => console.log("unhandledRejection")) // This is never called setTimeout(() => { Promise .reject("This is a promise rejection") .catch(() => console.log("Caught promise rejection")) // This is called }, 1)
Reacted by Dan Rose and ExE BossIt appears to be a debugger specific issue, as it does not emit an
uncaughtException(orunhandledRejection) event on the process:Yes, it is a debugger-specific issue caused by V8 "catch prediction". The debugger breaks synchronously when the promise is rejected.
It would be okay if the debugger paused here when
breakOnExceptionwas specified, but it's inappropriate forbreakOnUncaught.Related discussion:
https://issues.chromium.org/issues/41161875- addedv8 engineIssues and PRs related to the V8 dependency.Issues and PRs related to the V8 dependency.
on Jul 4, 2024 @nodejs/v8
Relatedly, the below code does not trip the debugger (with
breakOnUncaughtenabled) and does not print anything. I think this situation is dual to the originally reported issue:- the rejected promise never has a handler attached
- but catch prediction incorrectly expects the surrounding
trystatement to apply to the rejection (it doesn't because the promise is neverawaited).
setTimeout(async () => { try { Promise.reject() } catch (e) { } })
Reacted by Benjamin GruenbaumI think I figured out the
setTimeoutpart of this mystery. When running a module as the main entry point, the code is within in atryblock in anasyncfunction (inmodule_job.js).node/lib/internal/modules/esm/module_job.js
Lines 253 to 263 in 4b8000c
async run(isEntryPoint = false) { await this.instantiate(); if (isEntryPoint) { globalThis[entry_point_module_private_symbol] = this.module; } const timeout = -1; const breakOnSigint = false; setHasStartedUserESMExecution(); try { await this.module.evaluate(timeout, breakOnSigint); } catch (e) { Because of this
try/catchblock, NO promise rejection at module level is regarded by the inspector as an "uncaught exception", REGARDLESS OF WHETHER OR NOT IT HAS A HANDLER ATTACHED.I raised an issue for this catch prediction false positive but it was regarded as "infeasible" here: https://issues.chromium.org/issues/352455689
When code escapes this call stack by running asynchronously, the inspector no longer regards it as handled by that
try/catchso it may or may not be considered an "uncaught exception" (based on V8's designed behavior).Here's a demonstration:
// assume this is in a .mjs file // this function creates a Promise which is rejected and handled // This promise is NOT regarded as "caught" by the V8's catch prediction const makeRejectedPromise = (reason) => { Promise.reject(reason).catch(() => {}); }; // rejection regarded as handled created synchronously: makeRejectedPromise(1); (async () => { makeRejectedPromise(2); })(); await null; // or asynchronously after a module-level await makeRejectedPromise(3); // rejection regarded as unhandled when created asynchronously: (async () => { await null; makeRejectedPromise(4); })(); queueMicrotask(() => makeRejectedPromise(5)); process.nextTick(() => makeRejectedPromise(6)); setTimeout(() => makeRejectedPromise(7));
- changed the title
[-]Promise rejection created in timeout treated as unhandled by debugger[/-][+]Promise rejection created in timeout treated as unhandled by debugger (versus module-level)[/+]on Jul 22, 2024 - changed the title
[-]Promise rejection created in timeout treated as unhandled by debugger (versus module-level)[/-][+]Promise rejection in timeout (versus module-level) treated as unhandled by debugger[/+]on Jul 22, 2024 This appears to be fixed in Node 23.0.0.
The converse example given above still seems to be a problem: #53732 (comment)This also stops the inspector with unhandled rejection with node 20.19.0
async function test() { await Promise.race([ Promise.resolve().then(() => { return Promise.reject("error"); }) ]); } try { await test(); } catch (err) { console.error("caught", err); }This issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on May 4, 2026 I don't think this is stale
- removedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on May 5, 2026 This issue has been marked as stale due to 90 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Aug 3, 2026 not stale
- removedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Aug 4, 2026
Version
v22.4.0
Platform
Subsystem
No response
What steps will reproduce the bug?
Create this script:
And run it in the debugger with
break on Uncaughtenabled.How often does it reproduce? Is there a required condition?
Every time. Note this does not happen if
Promise.reject()is at top level instead of wrapped insetTimeout().What is the expected behavior? Why is that the expected behavior?
It is expected that the debugger does (1) not pause on such a promise or (2) only pauses on such a promise if
breakOnExceptionis enabled.The HTML spec guarantees that promise rejections are not considered unhandled if a handler is then synchronously attached.
What do you see instead?
Node breaks synchronously on the creation of the rejected promise. Similar issues happen when using the VSCode debugger and Chrome debugger.
Additional information
A downstream issue where this interferes with usage of the web streams API: #51093
I too have been incredibly confused by this, as it makes it seem like even correct usage of promises is developer error.
VSCode reports this as "Exception has occurred" instead of "promiseRejection".