Skip to content

fix(ext/node): fix node:domain across async boundaries#32897

Merged
bartlomieju merged 11 commits intomainfrom
fix/node-domain-timers-async-hooks
Mar 23, 2026
Merged

fix(ext/node): fix node:domain across async boundaries#32897
bartlomieju merged 11 commits intomainfrom
fix/node-domain-timers-async-hooks

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

@bartlomieju bartlomieju commented Mar 21, 2026

Summary

  • Fix node:domain not working across async boundaries (timers, I/O).
    process.domain was lost in timer callbacks and socket error handlers because
    timers didn't emit async hook lifecycle events and EventEmitter.emit didn't
    preserve domain context for error events with listeners.
  • Implement process.setUncaughtExceptionCaptureCallback() and
    process.hasUncaughtExceptionCaptureCallback() — needed by the domain module
    to route uncaught exceptions to the active domain's error handler.
  • Fix several Domain methods to match Node.js behavior: exit() (LIFO stack
    truncation), run() (set error metadata), bind()/intercept() (enter/exit
    domain, set domainBound/domainThrown/domain on errors).
  • Implement updateExceptionCapture() in domain.ts (was a TODO).
  • Fix EventEmitter.prototype.emit to wrap error event handlers with
    domain.enter()/domain.exit() when the emitter has a domain, preserving
    process.domain during handler execution.

Changes

ext/node/polyfills/internal/timers.mjs — Added async hook integration to
Timeout: emitInit on construction, emitBefore/emitAfter around callback
execution, emitDestroy on cleanup.

ext/node/polyfills/process.ts — Added
setUncaughtExceptionCaptureCallback/hasUncaughtExceptionCaptureCallback,
updated _fatalException to check the capture callback, updated
synchronizeListeners to register the global error handler when a capture
callback is set.

ext/node/polyfills/domain.ts — Implemented updateExceptionCapture(),
added domainUncaughtExceptionHandler with proper stack cleanup, fixed exit()
to truncate the stack from the domain's position (LIFO), fixed run() to set
domainThrown/domain on errors, fixed bind()/intercept() to enter/exit
domain and set error metadata, fixed patched EventEmitter.prototype.emit to
preserve domain context for error events with listeners.

Results

9 newly passing node_compat tests, 0 regressions:

  • test-domain-enter-exit.js
  • test-domain-error-types.js
  • test-domain-http-server.js
  • test-domain-intercept.js
  • test-domain-multiple-errors.js
  • test-domain-nexttick.js
  • test-domain-timer.js
  • test-domain-timers-uncaught-exception.js
  • test-domain-timers.js

Closes #27037 — the web-ext ECONNREFUSED retry pattern now works because
domain context is preserved across socket error handlers.
Closes #32915

bartlomieju and others added 7 commits March 21, 2026 17:49
- Add async hook integration (emitInit/emitBefore/emitAfter/emitDestroy) to
  Timeout class so that process.domain is preserved across setTimeout/setInterval
  callbacks
- Implement process.setUncaughtExceptionCaptureCallback and
  process.hasUncaughtExceptionCaptureCallback
- Update process._fatalException to check the capture callback before
  uncaughtException listeners
- Implement updateExceptionCapture() in domain.ts (was a TODO) to register the
  domain's uncaught exception handler via setUncaughtExceptionCaptureCallback
- Add domainUncaughtExceptionHandler that properly cleans up the domain stack
  and routes errors to the active domain

Fixes #27037

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… Node.js

- Domain.exit() now removes the domain AND all domains pushed after it
  (truncates stack from the domain's position, matching Node.js LIFO behavior)
- Domain.exit() no longer empties the stack when called on a domain not in it
- Domain.run() now sets domainThrown=true and domain property on errors
- Domain.bind() now enters/exits the domain and sets error metadata
  (domainBound, domainThrown, domain)
- Domain.intercept() now enters/exits the domain and sets error metadata

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a spec test verifying that process.domain is properly preserved
across setTimeout, setInterval, and nested timer callbacks, and that
domain error handlers catch errors thrown in timer callbacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 9 newly passing node:domain tests to the node_compat test suite:
- test-domain-enter-exit.js
- test-domain-error-types.js
- test-domain-http-server.js
- test-domain-intercept.js
- test-domain-multiple-errors.js
- test-domain-nexttick.js
- test-domain-timer.js
- test-domain-timers-uncaught-exception.js
- test-domain-timers.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… events

When an EventEmitter with a domain emits an 'error' event that has a
listener, wrap the listener call with domain.enter()/exit() so that
process.domain is set during the handler execution. This is critical for
the web-ext pattern where a socket error handler re-emits the error on a
parent EventEmitter that relies on domain context for error routing.

Adds a spec test reproducing the exact web-ext rdp-client.js pattern
from #27037.

Closes #27037

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… Timeout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Deno’s node:domain behavior to better match Node.js across async boundaries (timers and I/O), by integrating async_hooks lifecycle events and adding process-level uncaught exception capture support, plus new spec/node_compat coverage.

Changes:

  • Add async_hooks init/before/after/destroy integration for Timeout callbacks in the Node timers polyfill.
  • Implement process.setUncaughtExceptionCaptureCallback() / hasUncaughtExceptionCaptureCallback() and wire capture callback handling into _fatalException + listener synchronization.
  • Update node:domain semantics (stack unwinding, bind/intercept metadata, timer + EventEmitter error-context preservation) and add/enable new tests (spec + node_compat).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ext/node/polyfills/internal/timers.mjs Adds async_hooks lifecycle emissions for Timeout execution/cleanup.
ext/node/polyfills/process.ts Adds uncaught exception capture callback APIs and integrates them with fatal exception handling and listener wiring.
ext/node/polyfills/domain.ts Implements exception capture integration and adjusts domain stack + EventEmitter error handling to preserve domain context.
tests/node_compat/config.jsonc Enables additional Node domain tests now passing.
tests/specs/run/node_prefix_missing/node_globals.out Updates expected Timeout inspection output to include new async-related fields.
tests/specs/node/domain_timer/test.jsonc Adds new spec test cases for timer domain preservation and the web-ext retry pattern.
tests/specs/node/domain_timer/main.mjs Spec test verifying process.domain preservation and error routing in timers.
tests/specs/node/domain_timer/main.out Expected output for the timer-domain spec test.
tests/specs/node/domain_timer/webext_pattern.mjs Spec reproduction of the web-ext retry pattern relying on domains across async boundaries.
tests/specs/node/domain_timer/webext_pattern.out Expected output for the web-ext pattern spec test.

Comment thread ext/node/polyfills/domain.ts
Comment thread ext/node/polyfills/internal/timers.mjs
Comment thread ext/node/polyfills/domain.ts Outdated
Comment thread ext/node/polyfills/domain.ts
…exit()

- Domain.dispose() now sets this._disposed = true so that
  domainUncaughtExceptionHandler correctly skips disposed domains
- Domain.exit() now uses lastIndexOf (most recent occurrence) instead of
  indexOf, matching Node.js behavior when the same domain is entered
  multiple times

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bartlomieju and others added 3 commits March 22, 2026 11:17
)

## Summary
- Route `uncaughtExceptionHandler` through `process._fatalException` so
that `setUncaughtExceptionCaptureCallback` is respected for unhandled
rejections
- Install the unhandled rejection callback in `synchronizeListeners`
when a capture callback is active (not just when event listeners exist)
- Use proper Node.js error classes (`ERR_INVALID_ARG_TYPE`,
`ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET`) in
`setUncaughtExceptionCaptureCallback`
- Enable 4 newly passing node_compat tests

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju bartlomieju merged commit b75e2b8 into main Mar 23, 2026
220 of 222 checks passed
@bartlomieju bartlomieju deleted the fix/node-domain-timers-async-hooks branch March 23, 2026 09:40
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.

process.setUncaughtExceptionCaptureCallback not implemented Deno is not able to run npm:web-ext to run firefox extension

2 participants