Skip to content

Commit 7963ccb

Browse files
committed
repl: restore domain module compatibility
Add auxiliary callback mechanism to setUncaughtExceptionCaptureCallback to allow REPL and domain module to coexist. The REPL uses the new addUncaughtExceptionCaptureCallback API which doesn't conflict with domain's use of the primary callback. - Add addUncaughtExceptionCaptureCallback for non-exclusive callbacks - Update REPL to check for active domain before handling errors - Remove mutual exclusivity checks from domain module - Restore test-repl-domain.js test - Update domain coexistence tests
1 parent 855468d commit 7963ccb

File tree

7 files changed

+144
-78
lines changed

7 files changed

+144
-78
lines changed

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
@@ -306,6 +306,7 @@ ObjectDefineProperty(process, 'features', {
306306
const {
307307
onGlobalUncaughtException,
308308
setUncaughtExceptionCaptureCallback,
309+
addUncaughtExceptionCaptureCallback,
309310
hasUncaughtExceptionCaptureCallback,
310311
} = require('internal/process/execution');
311312

@@ -318,6 +319,8 @@ ObjectDefineProperty(process, 'features', {
318319
process._fatalException = onGlobalUncaughtException;
319320
process.setUncaughtExceptionCaptureCallback =
320321
setUncaughtExceptionCaptureCallback;
322+
process.addUncaughtExceptionCaptureCallback =
323+
addUncaughtExceptionCaptureCallback;
321324
process.hasUncaughtExceptionCaptureCallback =
322325
hasUncaughtExceptionCaptureCallback;
323326
}

lib/internal/process/execution.js

Lines changed: 39 additions & 5 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,
@@ -105,15 +106,18 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
105106
}
106107

107108
const exceptionHandlerState = {
108-
captureFn: null,
109+
captureFn: null, // Primary callback (for domain's exclusive use)
110+
auxiliaryCallbacks: [], // Auxiliary callbacks (for REPL, etc.) - always called
109111
reportFlag: false,
110112
};
111113

112114
function setUncaughtExceptionCaptureCallback(fn) {
113115
if (fn === null) {
114116
exceptionHandlerState.captureFn = fn;
115-
shouldAbortOnUncaughtToggle[0] = 1;
116-
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
117+
if (exceptionHandlerState.auxiliaryCallbacks.length === 0) {
118+
shouldAbortOnUncaughtToggle[0] = 1;
119+
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
120+
}
117121
return;
118122
}
119123
if (typeof fn !== 'function') {
@@ -129,6 +133,23 @@ function setUncaughtExceptionCaptureCallback(fn) {
129133
process.report.reportOnUncaughtException = false;
130134
}
131135

136+
// Add an auxiliary callback that coexists with the primary callback.
137+
// Auxiliary callbacks are called first; if any returns true, the error is handled.
138+
// Otherwise, the primary callback (if set) is called.
139+
function addUncaughtExceptionCaptureCallback(fn) {
140+
if (typeof fn !== 'function') {
141+
throw new ERR_INVALID_ARG_TYPE('fn', 'Function', fn);
142+
}
143+
if (exceptionHandlerState.auxiliaryCallbacks.length === 0 &&
144+
exceptionHandlerState.captureFn === null) {
145+
exceptionHandlerState.reportFlag =
146+
process.report.reportOnUncaughtException === true;
147+
process.report.reportOnUncaughtException = false;
148+
shouldAbortOnUncaughtToggle[0] = 0;
149+
}
150+
ArrayPrototypePush(exceptionHandlerState.auxiliaryCallbacks, fn);
151+
}
152+
132153
function hasUncaughtExceptionCaptureCallback() {
133154
return exceptionHandlerState.captureFn !== null;
134155
}
@@ -154,9 +175,21 @@ function createOnGlobalUncaughtException() {
154175

155176
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
156177
process.emit('uncaughtExceptionMonitor', er, type);
178+
let handled = false;
179+
// Primary callback (e.g., domain) has priority - it handles domain-specific errors
157180
if (exceptionHandlerState.captureFn !== null) {
158-
exceptionHandlerState.captureFn(er);
159-
} else if (!process.emit('uncaughtException', er, type)) {
181+
handled = exceptionHandlerState.captureFn(er);
182+
}
183+
// If primary didn't handle it, try auxiliary callbacks (e.g., REPL)
184+
if (!handled) {
185+
for (let i = exceptionHandlerState.auxiliaryCallbacks.length - 1; i >= 0; i--) {
186+
if (exceptionHandlerState.auxiliaryCallbacks[i](er) === true) {
187+
handled = true;
188+
break;
189+
}
190+
}
191+
}
192+
if (!handled && !process.emit('uncaughtException', er, type)) {
160193
// If someone handled it, then great. Otherwise, die in C++ land
161194
// since that means that we'll exit the process, emit the 'exit' event.
162195
try {
@@ -473,5 +506,6 @@ module.exports = {
473506
evalScript,
474507
onGlobalUncaughtException: createOnGlobalUncaughtException(),
475508
setUncaughtExceptionCaptureCallback,
509+
addUncaughtExceptionCaptureCallback,
476510
hasUncaughtExceptionCaptureCallback,
477511
};

lib/repl.js

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -187,36 +187,22 @@ let exceptionCaptureSetup = false;
187187
/**
188188
* Sets up the uncaught exception capture callback to route errors
189189
* to the appropriate REPL instance. This replaces domain-based error handling.
190-
* @returns {boolean} true if setup succeeded, false if capture callback already in use
190+
* Uses addUncaughtExceptionCaptureCallback to coexist with the primary
191+
* callback (e.g., domain module).
191192
*/
192193
function setupExceptionCapture() {
193-
if (exceptionCaptureSetup) return true;
194+
if (exceptionCaptureSetup) return;
194195

195-
if (process.hasUncaughtExceptionCaptureCallback()) {
196-
return false;
197-
}
198-
199-
process.setUncaughtExceptionCaptureCallback((err) => {
196+
process.addUncaughtExceptionCaptureCallback((err) => {
200197
const store = replContext.getStore();
201198
if (store?.replServer && !store.replServer.closed) {
202199
store.replServer._handleError(err);
203-
return;
204-
}
205-
// No active REPL context - re-emit for normal process handling
206-
process.setUncaughtExceptionCaptureCallback(null);
207-
exceptionCaptureSetup = false;
208-
try {
209-
if (!process.emit('uncaughtException', err, 'uncaughtException')) {
210-
// No handler, rethrow to exit process
211-
throw err;
212-
}
213-
} finally {
214-
setupExceptionCapture();
200+
return true; // We handled it
215201
}
202+
// No active REPL context - let other handlers try
216203
});
217204

218205
exceptionCaptureSetup = true;
219-
return true;
220206
}
221207

222208
const kBufferedCommandSymbol = Symbol('bufferedCommand');
@@ -624,6 +610,12 @@ class REPLServer extends Interface {
624610
}
625611
} catch (e) {
626612
err = e;
613+
// If there's an active domain with error listeners, let it handle the error
614+
if (process.domain?.listenerCount('error') > 0) {
615+
debug('domain handling error');
616+
process.domain.emit('error', err);
617+
return;
618+
}
627619
// Handle non-recoverable errors directly
628620
debug('not recoverable, handle error');
629621
self._handleError(err);
@@ -653,9 +645,15 @@ class REPLServer extends Interface {
653645
const result = (await promise)?.value;
654646
finishExecution(null, result);
655647
} catch (err) {
656-
// Handle non-recoverable async errors directly
657-
debug('not recoverable, handle error');
658-
self._handleError(err);
648+
// If there's an active domain with error listeners, let it handle the error
649+
if (process.domain?.listenerCount('error') > 0) {
650+
debug('domain handling async error');
651+
process.domain.emit('error', err);
652+
} else {
653+
// Handle non-recoverable async errors directly
654+
debug('not recoverable, handle error');
655+
self._handleError(err);
656+
}
659657
} finally {
660658
// Remove prioritized SIGINT listener if it was not called.
661659
prioritizedSigintQueue.delete(sigintListener);
Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
'use strict';
2+
// Tests that domain can be loaded after setUncaughtExceptionCaptureCallback
3+
// has been called. This verifies that the mutual exclusivity has been removed.
24
const common = require('../common');
3-
const assert = require('assert');
45

6+
// Set up a capture callback first
57
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
68

7-
assert.throws(
8-
() => require('domain'),
9-
{
10-
code: 'ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
11-
name: 'Error',
12-
message: /^A callback was registered.*with using the `domain` module/
13-
}
14-
);
9+
// Loading domain should not throw (coexistence is now supported)
10+
const domain = require('domain');
11+
12+
// Verify domain module loaded successfully
13+
const assert = require('assert');
14+
assert.ok(domain);
15+
assert.ok(domain.create);
1516

17+
// Clean up
1618
process.setUncaughtExceptionCaptureCallback(null);
17-
require('domain'); // Should not throw.
19+
20+
// Domain should still be usable
21+
const d = domain.create();
22+
assert.ok(d);
Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
11
'use strict';
2+
// Tests that setUncaughtExceptionCaptureCallback can be called after domain
3+
// is loaded. This verifies that the mutual exclusivity has been removed.
24
const common = require('../common');
35
const assert = require('assert');
46

5-
Error.stackTraceLimit = Infinity;
7+
// Load domain first
8+
const domain = require('domain');
9+
assert.ok(domain);
610

7-
(function foobar() {
8-
require('domain');
9-
})();
11+
// Setting callback should not throw (coexistence is now supported)
12+
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
1013

11-
assert.throws(
12-
() => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()),
13-
(err) => {
14-
common.expectsError(
15-
{
16-
code: 'ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
17-
name: 'Error',
18-
message: /^The `domain` module is in use, which is mutually/
19-
}
20-
)(err);
14+
// Verify callback is registered
15+
assert.ok(process.hasUncaughtExceptionCaptureCallback());
2116

22-
assert(err.stack.includes('-'.repeat(40)),
23-
`expected ${err.stack} to contain dashes`);
17+
// Clean up
18+
process.setUncaughtExceptionCaptureCallback(null);
19+
assert.ok(!process.hasUncaughtExceptionCaptureCallback());
2420

25-
const location = `at foobar (${__filename}:`;
26-
assert(err.stack.includes(location),
27-
`expected ${err.stack} to contain ${location}`);
28-
return true;
29-
}
30-
);
21+
// Domain should still be usable after callback operations
22+
const d = domain.create();
23+
assert.ok(d);

test/parallel/test-repl-domain.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
'use strict';
23+
require('../common');
24+
const { startNewREPLServer } = require('../common/repl');
25+
const ArrayStream = require('../common/arraystream');
26+
27+
const stream = new ArrayStream();
28+
29+
startNewREPLServer({
30+
input: stream,
31+
output: stream,
32+
terminal: false,
33+
});
34+
35+
stream.write = function(data) {
36+
// Don't use assert for this because the domain might catch it, and
37+
// give a false negative. Don't throw, just print and exit.
38+
if (data === 'OK\n') {
39+
console.log('ok');
40+
} else {
41+
console.error(data);
42+
process.exit(1);
43+
}
44+
};
45+
46+
stream.run([
47+
'require("domain").create().on("error", function() { console.log("OK") })' +
48+
'.run(function() { throw new Error("threw") })',
49+
]);

0 commit comments

Comments
 (0)