Skip to content

Commit 67b854d

Browse files
mcollinaaduh95
authored andcommitted
repl: remove dependency on domain module
Replace the domain-based error handling with AsyncLocalStorage and setUncaughtExceptionCaptureCallback. This removes the REPL's dependency on the deprecated domain module while preserving all existing behavior: - Synchronous errors during eval are caught and displayed - Async errors (setTimeout, promises, etc.) are caught via the uncaught exception capture callback - Top-level await errors are caught and displayed - The REPL continues operating after errors - Multiple REPL instances can coexist with errors routed correctly Changes: - Use AsyncLocalStorage to track which REPL instance owns an async context, replacing domain's automatic async tracking - Add setupExceptionCapture() to install setUncaughtExceptionCaptureCallback for catching async errors and routing them to the correct REPL - Extract error handling logic into REPLServer.prototype._handleError() - Wrap eval execution in replContext.run() for async context tracking - Update newListener protection to check AsyncLocalStorage context - Throw ERR_INVALID_ARG_VALUE if options.domain is passed PR-URL: #61227 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
1 parent 62d2cd4 commit 67b854d

26 files changed

+417
-289
lines changed

doc/api/process.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,44 @@ generate a core file.
736736

737737
This feature is not available in [`Worker`][] threads.
738738

739+
## `process.addUncaughtExceptionCaptureCallback(fn)`
740+
741+
<!-- YAML
742+
added: REPLACEME
743+
-->
744+
745+
> Stability: 1 - Experimental
746+
747+
* `fn` {Function}
748+
749+
The `process.addUncaughtExceptionCaptureCallback()` function adds a callback
750+
that will be invoked when an uncaught exception occurs, receiving the exception
751+
value as its first argument.
752+
753+
Unlike [`process.setUncaughtExceptionCaptureCallback()`][], this function allows
754+
multiple callbacks to be registered and does not conflict with the
755+
[`domain`][] module. Callbacks are called in reverse order of registration
756+
(most recent first). If a callback returns `true`, subsequent callbacks
757+
and the default uncaught exception handling are skipped.
758+
759+
```mjs
760+
import process from 'node:process';
761+
762+
process.addUncaughtExceptionCaptureCallback((err) => {
763+
console.error('Caught exception:', err.message);
764+
return true; // Indicates exception was handled
765+
});
766+
```
767+
768+
```cjs
769+
const process = require('node:process');
770+
771+
process.addUncaughtExceptionCaptureCallback((err) => {
772+
console.error('Caught exception:', err.message);
773+
return true; // Indicates exception was handled
774+
});
775+
```
776+
739777
## `process.allowedNodeEnvironmentFlags`
740778

741779
<!-- YAML
@@ -4015,6 +4053,11 @@ This implies calling `module.setSourceMapsSupport()` with an option
40154053
40164054
<!-- YAML
40174055
added: v9.3.0
4056+
changes:
4057+
- version: REPLACEME
4058+
pr-url: https://github.com/nodejs/node/pull/61227
4059+
description: Use `process.addUncaughtExceptionCaptureCallback()` to
4060+
register multiple callbacks.
40184061
-->
40194062
40204063
* `fn` {Function|null}
@@ -4034,8 +4077,8 @@ To unset the capture function,
40344077
method with a non-`null` argument while another capture function is set will
40354078
throw an error.
40364079
4037-
Using this function is mutually exclusive with using the deprecated
4038-
[`domain`][] built-in module.
4080+
To register multiple callbacks that can coexist, use
4081+
[`process.addUncaughtExceptionCaptureCallback()`][] instead.
40394082
40404083
## `process.sourceMapsEnabled`
40414084
@@ -4567,6 +4610,7 @@ cases:
45674610
[`net.Socket`]: net.md#class-netsocket
45684611
[`os.constants.dlopen`]: os.md#dlopen-constants
45694612
[`postMessageToThread()`]: worker_threads.md#worker_threadspostmessagetothreadthreadid-value-transferlist-timeout
4613+
[`process.addUncaughtExceptionCaptureCallback()`]: #processadduncaughtexceptioncapturecallbackfn
45704614
[`process.argv`]: #processargv
45714615
[`process.config`]: #processconfig
45724616
[`process.execPath`]: #processexecpath

lib/domain.js

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,11 @@ const {
4040
ReflectApply,
4141
SafeMap,
4242
SafeWeakMap,
43-
StringPrototypeRepeat,
4443
Symbol,
4544
} = primordials;
4645

4746
const EventEmitter = require('events');
4847
const {
49-
ERR_DOMAIN_CALLBACK_NOT_AVAILABLE,
50-
ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE,
5148
ERR_UNHANDLED_ERROR,
5249
} = require('internal/errors').codes;
5350
const { createHook } = require('async_hooks');
@@ -119,22 +116,9 @@ const asyncHook = createHook({
119116
},
120117
});
121118

122-
// When domains are in use, they claim full ownership of the
123-
// uncaught exception capture callback.
124-
if (process.hasUncaughtExceptionCaptureCallback()) {
125-
throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE();
126-
}
127-
128-
// Get the stack trace at the point where `domain` was required.
129-
// eslint-disable-next-line no-restricted-syntax
130-
const domainRequireStack = new Error('require(`domain`) at this point').stack;
131-
119+
// Domain uses the stacking capability of setUncaughtExceptionCaptureCallback
120+
// to coexist with other callbacks (e.g., REPL).
132121
const { setUncaughtExceptionCaptureCallback } = process;
133-
process.setUncaughtExceptionCaptureCallback = function(fn) {
134-
const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE();
135-
err.stack += `\n${StringPrototypeRepeat('-', 40)}\n${domainRequireStack}`;
136-
throw err;
137-
};
138122

139123

140124
let sendMakeCallbackDeprecation = false;

lib/internal/bootstrap/node.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ ObjectDefineProperty(process, 'features', {
307307
const {
308308
onGlobalUncaughtException,
309309
setUncaughtExceptionCaptureCallback,
310+
addUncaughtExceptionCaptureCallback,
310311
hasUncaughtExceptionCaptureCallback,
311312
} = require('internal/process/execution');
312313

@@ -319,6 +320,8 @@ ObjectDefineProperty(process, 'features', {
319320
process._fatalException = onGlobalUncaughtException;
320321
process.setUncaughtExceptionCaptureCallback =
321322
setUncaughtExceptionCaptureCallback;
323+
process.addUncaughtExceptionCaptureCallback =
324+
addUncaughtExceptionCaptureCallback;
322325
process.hasUncaughtExceptionCaptureCallback =
323326
hasUncaughtExceptionCaptureCallback;
324327
}

lib/internal/process/execution.js

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypePush,
45
RegExpPrototypeExec,
56
StringPrototypeIndexOf,
67
StringPrototypeSlice,
@@ -17,6 +18,7 @@ const {
1718
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET,
1819
},
1920
} = require('internal/errors');
21+
const { validateFunction } = require('internal/validators');
2022
const { pathToFileURL } = require('internal/url');
2123
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
2224
const {
@@ -105,15 +107,18 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
105107
}
106108

107109
const exceptionHandlerState = {
108-
captureFn: null,
110+
captureFn: null, // Primary callback (for domain's exclusive use)
111+
auxiliaryCallbacks: [], // Auxiliary callbacks (for REPL, etc.) - always called
109112
reportFlag: false,
110113
};
111114

112115
function setUncaughtExceptionCaptureCallback(fn) {
113116
if (fn === null) {
114117
exceptionHandlerState.captureFn = fn;
115-
shouldAbortOnUncaughtToggle[0] = 1;
116-
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
118+
if (exceptionHandlerState.auxiliaryCallbacks.length === 0) {
119+
shouldAbortOnUncaughtToggle[0] = 1;
120+
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
121+
}
117122
return;
118123
}
119124
if (typeof fn !== 'function') {
@@ -129,6 +134,21 @@ function setUncaughtExceptionCaptureCallback(fn) {
129134
process.report.reportOnUncaughtException = false;
130135
}
131136

137+
// Add an auxiliary callback that coexists with the primary callback.
138+
// Auxiliary callbacks are called first; if any returns true, the error is handled.
139+
// Otherwise, the primary callback (if set) is called.
140+
function addUncaughtExceptionCaptureCallback(fn) {
141+
validateFunction(fn, 'fn');
142+
if (exceptionHandlerState.auxiliaryCallbacks.length === 0 &&
143+
exceptionHandlerState.captureFn === null) {
144+
exceptionHandlerState.reportFlag =
145+
process.report.reportOnUncaughtException === true;
146+
process.report.reportOnUncaughtException = false;
147+
shouldAbortOnUncaughtToggle[0] = 0;
148+
}
149+
ArrayPrototypePush(exceptionHandlerState.auxiliaryCallbacks, fn);
150+
}
151+
132152
function hasUncaughtExceptionCaptureCallback() {
133153
return exceptionHandlerState.captureFn !== null;
134154
}
@@ -154,21 +174,33 @@ function createOnGlobalUncaughtException() {
154174

155175
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
156176
process.emit('uncaughtExceptionMonitor', er, type);
177+
// Primary callback (e.g., domain) has priority and always handles the exception
157178
if (exceptionHandlerState.captureFn !== null) {
158179
exceptionHandlerState.captureFn(er);
159-
} else if (!process.emit('uncaughtException', er, type)) {
160-
// If someone handled it, then great. Otherwise, die in C++ land
161-
// since that means that we'll exit the process, emit the 'exit' event.
162-
try {
163-
if (!process._exiting) {
164-
process._exiting = true;
165-
process.exitCode = kGenericUserError;
166-
process.emit('exit', kGenericUserError);
180+
} else {
181+
// If no primary callback, try auxiliary callbacks (e.g., REPL)
182+
// They must return true to indicate handling
183+
let handled = false;
184+
for (let i = exceptionHandlerState.auxiliaryCallbacks.length - 1; i >= 0; i--) {
185+
if (exceptionHandlerState.auxiliaryCallbacks[i](er) === true) {
186+
handled = true;
187+
break;
188+
}
189+
}
190+
if (!handled && !process.emit('uncaughtException', er, type)) {
191+
// If someone handled it, then great. Otherwise, die in C++ land
192+
// since that means that we'll exit the process, emit the 'exit' event.
193+
try {
194+
if (!process._exiting) {
195+
process._exiting = true;
196+
process.exitCode = kGenericUserError;
197+
process.emit('exit', kGenericUserError);
198+
}
199+
} catch {
200+
// Nothing to be done about it at this point.
167201
}
168-
} catch {
169-
// Nothing to be done about it at this point.
202+
return false;
170203
}
171-
return false;
172204
}
173205

174206
// If we handled an error, then make sure any ticks get processed
@@ -477,5 +509,6 @@ module.exports = {
477509
evalScript,
478510
onGlobalUncaughtException: createOnGlobalUncaughtException(),
479511
setUncaughtExceptionCaptureCallback,
512+
addUncaughtExceptionCaptureCallback,
480513
hasUncaughtExceptionCaptureCallback,
481514
};

0 commit comments

Comments
 (0)