fix(ext/node): fix node:domain across async boundaries#32897
Merged
bartlomieju merged 11 commits intomainfrom Mar 23, 2026
Merged
fix(ext/node): fix node:domain across async boundaries#32897bartlomieju merged 11 commits intomainfrom
bartlomieju merged 11 commits intomainfrom
Conversation
- 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>
Contributor
There was a problem hiding this comment.
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
Timeoutcallbacks in the Node timers polyfill. - Implement
process.setUncaughtExceptionCaptureCallback()/hasUncaughtExceptionCaptureCallback()and wire capture callback handling into_fatalException+ listener synchronization. - Update
node:domainsemantics (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. |
…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>
4 tasks
) ## 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
node:domainnot working across async boundaries (timers, I/O).process.domainwas lost in timer callbacks and socket error handlers becausetimers didn't emit async hook lifecycle events and
EventEmitter.emitdidn'tpreserve domain context for error events with listeners.
process.setUncaughtExceptionCaptureCallback()andprocess.hasUncaughtExceptionCaptureCallback()— needed by the domain moduleto route uncaught exceptions to the active domain's error handler.
Domainmethods to match Node.js behavior:exit()(LIFO stacktruncation),
run()(set error metadata),bind()/intercept()(enter/exitdomain, set
domainBound/domainThrown/domainon errors).updateExceptionCapture()in domain.ts (was a TODO).EventEmitter.prototype.emitto wrap error event handlers withdomain.enter()/domain.exit()when the emitter has a domain, preservingprocess.domainduring handler execution.Changes
ext/node/polyfills/internal/timers.mjs— Added async hook integration toTimeout:emitIniton construction,emitBefore/emitAfteraround callbackexecution,
emitDestroyon cleanup.ext/node/polyfills/process.ts— AddedsetUncaughtExceptionCaptureCallback/hasUncaughtExceptionCaptureCallback,updated
_fatalExceptionto check the capture callback, updatedsynchronizeListenersto register the global error handler when a capturecallback is set.
ext/node/polyfills/domain.ts— ImplementedupdateExceptionCapture(),added
domainUncaughtExceptionHandlerwith proper stack cleanup, fixedexit()to truncate the stack from the domain's position (LIFO), fixed
run()to setdomainThrown/domainon errors, fixedbind()/intercept()to enter/exitdomain and set error metadata, fixed patched
EventEmitter.prototype.emittopreserve domain context for error events with listeners.
Results
9 newly passing
node_compattests, 0 regressions:test-domain-enter-exit.jstest-domain-error-types.jstest-domain-http-server.jstest-domain-intercept.jstest-domain-multiple-errors.jstest-domain-nexttick.jstest-domain-timer.jstest-domain-timers-uncaught-exception.jstest-domain-timers.jsCloses #27037 — the web-ext
ECONNREFUSEDretry pattern now works becausedomain context is preserved across socket error handlers.
Closes #32915