Skip to content

Commit fe102f0

Browse files
committed
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
1 parent 5536325 commit fe102f0

21 files changed

+304
-270
lines changed

lib/repl.js

Lines changed: 198 additions & 141 deletions
Large diffs are not rendered by default.

test/common/repl.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
const ArrayStream = require('../common/arraystream');
44
const repl = require('node:repl');
5-
const assert = require('node:assert');
65

7-
function startNewREPLServer(replOpts = {}, testingOpts = {}) {
6+
function startNewREPLServer(replOpts = {}) {
87
const input = new ArrayStream();
98
const output = new ArrayStream();
109

@@ -20,11 +19,6 @@ function startNewREPLServer(replOpts = {}, testingOpts = {}) {
2019
...replOpts,
2120
});
2221

23-
if (!testingOpts.disableDomainErrorAssert) {
24-
// Some errors are passed to the domain, but do not callback
25-
replServer._domain.on('error', assert.ifError);
26-
}
27-
2822
return { replServer, input, output };
2923
}
3024

test/fixtures/repl-tab-completion-nested-repls.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Tab completion sometimes uses a separate REPL instance under the hood.
2-
// That REPL instance has its own domain. Make sure domain errors trickle back
3-
// up to the main REPL.
2+
// Make sure errors in completion callbacks are properly thrown.
43
//
54
// Ref: https://github.com/nodejs/node/issues/21586
65

@@ -31,11 +30,6 @@ const repl = require('repl');
3130
const putIn = new ArrayStream();
3231
const testMe = repl.start('', putIn);
3332

34-
// Some errors are passed to the domain, but do not callback.
35-
testMe._domain.on('error', function(err) {
36-
throw err;
37-
});
38-
3933
// Nesting of structures causes REPL to use a nested REPL for completion.
4034
putIn.run([
4135
'var top = function() {',

test/parallel/test-repl-domain.js

Lines changed: 0 additions & 49 deletions
This file was deleted.

test/parallel/test-repl-eval-error-after-close.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ const assert = require('node:assert');
2020
eval$.resolve();
2121
});
2222
},
23-
}, {
24-
disableDomainErrorAssert: true,
2523
});
2624

2725
replServer.write('\n');

test/parallel/test-repl-let-process.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ require('../common');
33
const { startNewREPLServer } = require('../common/repl');
44

55
// Regression test for https://github.com/nodejs/node/issues/6802
6-
const { input } = startNewREPLServer({ useGlobal: true }, { disableDomainErrorAssert: true });
6+
const { input } = startNewREPLServer({ useGlobal: true });
77
input.run(['let process']);

test/parallel/test-repl-mode.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ function testSloppyMode() {
3030
}
3131

3232
function testStrictMode() {
33-
const { input, output } = startNewREPLServer({ replMode: repl.REPL_MODE_STRICT, terminal: false, prompt: '> ' }, {
34-
disableDomainErrorAssert: true,
33+
const { input, output } = startNewREPLServer({
34+
replMode: repl.REPL_MODE_STRICT, terminal: false, prompt: '> '
3535
});
3636

3737
input.emit('data', 'x = 3\n');
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use strict';
2+
3+
// This test verifies that when multiple REPL instances exist concurrently,
4+
// async errors are correctly routed to the REPL instance that created them.
5+
6+
const common = require('../common');
7+
const assert = require('assert');
8+
const repl = require('repl');
9+
const { Writable, PassThrough } = require('stream');
10+
11+
// Create two REPLs with separate inputs and outputs
12+
let output1 = '';
13+
let output2 = '';
14+
15+
const input1 = new PassThrough();
16+
const input2 = new PassThrough();
17+
18+
const writable1 = new Writable({
19+
write(chunk, encoding, callback) {
20+
output1 += chunk.toString();
21+
callback();
22+
}
23+
});
24+
25+
const writable2 = new Writable({
26+
write(chunk, encoding, callback) {
27+
output2 += chunk.toString();
28+
callback();
29+
}
30+
});
31+
32+
const r1 = repl.start({
33+
input: input1,
34+
output: writable1,
35+
terminal: false,
36+
prompt: 'R1> ',
37+
});
38+
39+
const r2 = repl.start({
40+
input: input2,
41+
output: writable2,
42+
terminal: false,
43+
prompt: 'R2> ',
44+
});
45+
46+
// Create async error in REPL 1
47+
input1.write('setTimeout(() => { throw new Error("error from repl1") }, 10)\n');
48+
49+
// Create async error in REPL 2
50+
input2.write('setTimeout(() => { throw new Error("error from repl2") }, 20)\n');
51+
52+
setTimeout(common.mustCall(() => {
53+
r1.close();
54+
r2.close();
55+
56+
// Verify error from REPL 1 went to REPL 1's output
57+
assert.match(output1, /error from repl1/,
58+
'REPL 1 should have received its own async error');
59+
60+
// Verify error from REPL 2 went to REPL 2's output
61+
assert.match(output2, /error from repl2/,
62+
'REPL 2 should have received its own async error');
63+
64+
// Verify errors did not cross over to wrong REPL
65+
assert.doesNotMatch(output1, /error from repl2/,
66+
'REPL 1 should not have received REPL 2\'s error');
67+
assert.doesNotMatch(output2, /error from repl1/,
68+
'REPL 2 should not have received REPL 1\'s error');
69+
}), 100);

test/parallel/test-repl-pretty-custom-stack.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ function run({ command, expected }) {
1010
const { replServer, output } = startNewREPLServer({
1111
terminal: false,
1212
useColors: false
13-
}, {
14-
disableDomainErrorAssert: true,
1513
});
1614

1715
replServer.write(`${command}\n`);

test/parallel/test-repl-pretty-stack-custom-writer.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ const { startNewREPLServer } = require('../common/repl');
55

66
const testingReplPrompt = '_REPL_TESTING_PROMPT_>';
77

8-
const { replServer, output } = startNewREPLServer(
9-
{ prompt: testingReplPrompt },
10-
{ disableDomainErrorAssert: true }
11-
);
8+
const { replServer, output } = startNewREPLServer({ prompt: testingReplPrompt });
129

1310
replServer.write('throw new Error("foo[a]")\n');
1411

0 commit comments

Comments
 (0)