Skip to content

fix(ext/node): implement v8.promiseHooks API - #36281

Merged
bartlomieju merged 3 commits into
mainfrom
feat/v8-promise-hooks
Jul 27, 2026
Merged

bartlomieju merged 3 commits into
mainfrom
feat/v8-promise-hooks

Conversation

@bartlomieju

Copy link
Copy Markdown
Member

Summary

Implements the Node.js v8.promiseHooks API β€” onInit, onBefore, onAfter, onSettled, and createHook β€” on top of Deno core's existing core.setPromiseHooks V8 bindings.

This is the clean, self-contained half of #32905, carved out and reimplemented against current main (the original PR was based on a since-rewritten v8.ts and process.ts). The --unhandled-rejections half will follow separately, rebuilt on top of the current synchronizeListeners.

Implementation notes

  • Validation: rejects non-functions, async functions, and async generators, matching Node's error message (The "<name>" argument must be of type function).
  • Re-entrancy guard: V8 promise hooks fire for all promise operations, including promises created inside the hooks themselves β€” a flag prevents infinite recursion.
  • Single-registration dispatcher: core.setPromiseHooks is additive-only (no removal), so we install permanent dispatchers once that consult the current hook arrays. The stop function returned by each registration splices the hook back out.

Tests

Enables two node_compat tests, both passing:

  • parallel/test-promise-hook-on-init.js
  • parallel/test-promise-hook-on-before.js

The remaining hook tests (on-after, on-resolve, create-hook) are left disabled: they assert exact hook-invocation counts that Deno's internal runtime promises perturb, and need further work to reconcile.

Test plan

  • cargo test -p node_compat_tests test-promise-hook-on-init β€” passes
  • cargo test -p node_compat_tests test-promise-hook-on-before β€” passes
  • ./tools/format.js && ./tools/lint.js --js clean

πŸ€– Generated with Claude Code

Implement the Node.js `v8.promiseHooks` API (`onInit`, `onBefore`,
`onAfter`, `onSettled`, `createHook`) on top of Deno core's existing
`core.setPromiseHooks` V8 bindings.

Details:
- Async function / async generator validation (they cannot be hooks)
- Re-entrancy guard, since V8 promise hooks fire for all promises,
  including those created inside the hooks themselves
- Single-registration dispatcher pattern, because the core API is
  additive-only (no removal); the permanent dispatchers consult the
  current hook arrays and stop functions splice hooks back out

Enables the `test-promise-hook-on-init.js` and
`test-promise-hook-on-before.js` node_compat tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bartlomieju

Copy link
Copy Markdown
Member Author

Reviewed the promiseHooks implementation β€” the core mechanism is sound (correct setPromiseHooks(init, before, after, resolve) arg order, the settled β†’ resolve mapping is right, the length-guarded permanent dispatchers keep the never-uninstalled hooks cheap, and the global inPromiseHook re-entrancy guard is fine since cross-type events only fire via microtasks after it resets). Two issues on the mutation/removal paths worth addressing:

1. A hook that removes itself mid-dispatch skips a sibling hook.

The dispatcher loops re-read .length each iteration and iterate the live array while removeHook splices it in place:

for (let i = 0; i < initHooks.length; i++) {
  initHooks[i](promise, parent);
}

Register init hooks A and B, and have A's body call its own stop(). On an init event: i=0 calls A β†’ A splices index 0 β†’ array becomes [B] (length 1) β†’ i=1, 1 < 1 is false β†’ loop ends and B never fires for that promise, even though it's a live registered hook. Node snapshots the hook list before invoking to avoid this. Fix: iterate over a copy, e.g. ArrayPrototypeSlice(initHooks, 0).

2. createHook leaks partially-registered hooks when a later callback fails validation.

createHook validates-and-pushes init, then before, etc. sequentially:

createHook({ init: goodFn, before: 123 })

pushes goodFn into initHooks, then throws on before's validatePlainFunction. goodFn is now permanently registered (fires on every promise init) and no stop function is returned to remove it. Fix: validate all provided callbacks up front, before any ArrayPrototypePush β€” matching the validate-then-mutate order the single-hook setters already use. (Node may share this latent behavior via its per-setter registration, so it's partly a parity call, but it's cheap to prevent here.)

Everything else checked out: double-calling a returned stop() is harmless (indexOf returns -1), and primordials usage is consistent with the rest of the file.

@bartlomieju bartlomieju changed the title feat(ext/node): implement v8.promiseHooks API fix(ext/node): implement v8.promiseHooks API Jul 24, 2026
- Dispatchers now iterate a snapshot of each hook list, so a hook that
  removes itself (or a sibling) mid-dispatch no longer shifts indices and
  skips a still-registered hook.
- `promiseHooks.createHook` validates every provided callback before
  registering any, so a later validation failure can't leave earlier hooks
  permanently registered with no stop function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bartlomieju

Copy link
Copy Markdown
Member Author

Thanks for the review β€” both addressed in cc6add4:

  1. Self-removing hook skipping a sibling. The four dispatchers now snapshot the hook list (ArrayPrototypeSlice(hooks, 0)) before iterating, so a hook that calls its own stop() (or removes another) mid-dispatch no longer shifts indices out from under the remaining hooks. Verified: with init hooks A and B where A stops itself, both A and B fire on the same promise.

  2. createHook leaking partially-registered hooks. It now validates every provided callback first, then pushes them β€” so createHook({ init: goodFn, before: 123 }) throws without registering goodFn. Verified: after the throw, the init hook count stays 0.

test-promise-hook-on-init.js / -on-before.js still pass.

@bartlomieju
bartlomieju merged commit 861c8c9 into main Jul 27, 2026
135 checks passed
@bartlomieju
bartlomieju deleted the feat/v8-promise-hooks branch July 27, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant