Skip to content

src: skip JS callback for settled Promise.race losers#62336

Open
Felipeness wants to merge 2 commits intonodejs:mainfrom
Felipeness:fix/promise-race-oom
Open

src: skip JS callback for settled Promise.race losers#62336
Felipeness wants to merge 2 commits intonodejs:mainfrom
Felipeness:fix/promise-race-oom

Conversation

@Felipeness
Copy link

Summary

Fixes #51452

When Promise.race() settles, V8 fires kPromiseResolveAfterResolved / kPromiseRejectAfterResolved for each "losing" promise. Node's PromiseRejectCallback in src/node_task_queue.cc was crossing into JS for these events, but since the multipleResolves event reached EOL in Node v25 (PR #58707), the JS handler does nothing — it just breaks.

The unnecessary C++-to-JS boundary crossings accumulate references in a tight loop, causing OOM when using Promise.race() with immediately-resolving promises.

Fix

Return early in PromiseRejectCallback() for kPromiseResolveAfterResolved and kPromiseRejectAfterResolved, skipping the JS callback entirely. Also remove the dead case branches and unused constant imports from the JS side.

Previous attempts

This PR resubmits the fix with a regression test.

Test

Added test/parallel/test-promise-race-memory-leak.js that runs 100k iterations of Promise.race() with immediately-resolving promises under --max-old-space-size=20. Before the fix, this OOMs; after the fix, it completes normally.

Refs: #51452
Refs: #60184
Refs: #61960

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. process Issues and PRs related to the process subsystem. labels Mar 19, 2026
When Promise.race() or Promise.any() settles, V8 fires
kPromiseResolveAfterResolved / kPromiseRejectAfterResolved for each
"losing" promise. The PromiseRejectCallback in node_task_queue.cc was
crossing into JS for these events, but since the multipleResolves
event reached EOL in v25 (PR nodejs#58707), the JS handler does nothing.

The unnecessary C++-to-JS boundary crossings accumulate references in
a tight loop, causing OOM when using Promise.race() with
immediately-resolving promises.

Return early in PromiseRejectCallback() for these two events, skipping
the JS callback entirely. Also remove the dead case branches and
unused constant imports from the JS side.

Fixes: nodejs#51452
Refs: nodejs#60184
Refs: nodejs#61960
@Felipeness Felipeness force-pushed the fix/promise-race-oom branch from debf239 to 7033537 Compare March 19, 2026 14:08
Move early returns for kPromiseResolveAfterResolved and
kPromiseRejectAfterResolved before Number::New and CHECK(!callback),
avoiding unnecessary work. Remove dead NODE_DEFINE_CONSTANT exports
and fix comment placement in the JS switch statement. Bump test
--max-old-space-size from 20 to 64 for safety on instrumented builds.
@bakkot
Copy link
Contributor

bakkot commented Mar 19, 2026

I have an open CL for a patch to V8 which would effectively remove these events on the V8 side, incidentally.

@Felipeness
Copy link
Author

That's great context, thanks for sharing! Took a look at your CL, really cool to see the root cause being addressed on the V8 side.

I think both changes complement each other well. This Node-side fix is a small early return for events that have been no-ops since multipleResolves was removed in v25, so it can ship now and help users already hitting the OOM. Once your V8 change lands and rolls into Node, the early return just becomes unreachable and can be cleaned up easily.

Would you mind linking your CL in #51452 as well? Would be great context for anyone following the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. process Issues and PRs related to the process subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Racing immediately-resolving Promises leads to memory leak

3 participants