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: https://github.com/nodejs/node/pull/61227
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Matteo Collina authored and GitHub committed 2026-03-06 15:38:36 +00:00
1 parent 798797aa89
commit a9da9ffc04
26 files changed
+417 -289

No files matched your search

+46 -2
View File
@@ -736,6 +736,44 @@ generate a core file.
This feature is not available in [`Worker`][] threads.
## `process.addUncaughtExceptionCaptureCallback(fn)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `fn` {Function}
The `process.addUncaughtExceptionCaptureCallback()` function adds a callback
that will be invoked when an uncaught exception occurs, receiving the exception
value as its first argument.
Unlike [`process.setUncaughtExceptionCaptureCallback()`][], this function allows
multiple callbacks to be registered and does not conflict with the
[`domain`][] module. Callbacks are called in reverse order of registration
(most recent first). If a callback returns `true`, subsequent callbacks
and the default uncaught exception handling are skipped.
```mjs
import process from 'node:process';
process.addUncaughtExceptionCaptureCallback((err) => {
console.error('Caught exception:', err.message);
return true; // Indicates exception was handled
});
```
```cjs
const process = require('node:process');
process.addUncaughtExceptionCaptureCallback((err) => {
console.error('Caught exception:', err.message);
return true; // Indicates exception was handled
});
```
## `process.allowedNodeEnvironmentFlags`
<!-- YAML
@@ -4021,6 +4059,11 @@ This implies calling `module.setSourceMapsSupport()` with an option
<!-- YAML
added: v9.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/61227
description: Use `process.addUncaughtExceptionCaptureCallback()` to
register multiple callbacks.
-->
* `fn` {Function|null}
@@ -4040,8 +4083,8 @@ To unset the capture function,
method with a non-`null` argument while another capture function is set will
throw an error.
Using this function is mutually exclusive with using the deprecated
[`domain`][] built-in module.
To register multiple callbacks that can coexist, use
[`process.addUncaughtExceptionCaptureCallback()`][] instead.
## `process.sourceMapsEnabled`
@@ -4573,6 +4616,7 @@ cases:
[`net.Socket`]: net.md#class-netsocket
[`os.constants.dlopen`]: os.md#dlopen-constants
[`postMessageToThread()`]: worker_threads.md#worker_threadspostmessagetothreadthreadid-value-transferlist-timeout
[`process.addUncaughtExceptionCaptureCallback()`]: #processadduncaughtexceptioncapturecallbackfn
[`process.argv`]: #processargv
[`process.config`]: #processconfig
[`process.execPath`]: #processexecpath
+2 -18
View File
@@ -40,14 +40,11 @@ const {
ReflectApply,
SafeMap,
SafeWeakMap,
StringPrototypeRepeat,
Symbol,
} = primordials;
const EventEmitter = require('events');
const {
ERR_DOMAIN_CALLBACK_NOT_AVAILABLE,
ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE,
ERR_UNHANDLED_ERROR,
} = require('internal/errors').codes;
const { createHook } = require('async_hooks');
@@ -119,22 +116,9 @@ const asyncHook = createHook({
},
});
// When domains are in use, they claim full ownership of the
// uncaught exception capture callback.
if (process.hasUncaughtExceptionCaptureCallback()) {
throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE();
}
// Get the stack trace at the point where `domain` was required.
// eslint-disable-next-line no-restricted-syntax
const domainRequireStack = new Error('require(`domain`) at this point').stack;
// Domain uses the stacking capability of setUncaughtExceptionCaptureCallback
// to coexist with other callbacks (e.g., REPL).
const { setUncaughtExceptionCaptureCallback } = process;
process.setUncaughtExceptionCaptureCallback = function(fn) {
const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE();
err.stack += `\n${StringPrototypeRepeat('-', 40)}\n${domainRequireStack}`;
throw err;
};
let sendMakeCallbackDeprecation = false;
+3
View File
@@ -307,6 +307,7 @@ ObjectDefineProperty(process, 'features', {
const {
onGlobalUncaughtException,
setUncaughtExceptionCaptureCallback,
addUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');
@@ -319,6 +320,8 @@ ObjectDefineProperty(process, 'features', {
process._fatalException = onGlobalUncaughtException;
process.setUncaughtExceptionCaptureCallback =
setUncaughtExceptionCaptureCallback;
process.addUncaughtExceptionCaptureCallback =
addUncaughtExceptionCaptureCallback;
process.hasUncaughtExceptionCaptureCallback =
hasUncaughtExceptionCaptureCallback;
}
+47 -14
View File
@@ -1,6 +1,7 @@
'use strict';
const {
ArrayPrototypePush,
RegExpPrototypeExec,
StringPrototypeIndexOf,
StringPrototypeSlice,
@@ -17,6 +18,7 @@ const {
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET,
},
} = require('internal/errors');
const { validateFunction } = require('internal/validators');
const { pathToFileURL } = require('internal/url');
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const {
@@ -105,15 +107,18 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
}
const exceptionHandlerState = {
captureFn: null,
captureFn: null, // Primary callback (for domain's exclusive use)
auxiliaryCallbacks: [], // Auxiliary callbacks (for REPL, etc.) - always called
reportFlag: false,
};
function setUncaughtExceptionCaptureCallback(fn) {
if (fn === null) {
exceptionHandlerState.captureFn = fn;
shouldAbortOnUncaughtToggle[0] = 1;
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
if (exceptionHandlerState.auxiliaryCallbacks.length === 0) {
shouldAbortOnUncaughtToggle[0] = 1;
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
}
return;
}
if (typeof fn !== 'function') {
@@ -129,6 +134,21 @@ function setUncaughtExceptionCaptureCallback(fn) {
process.report.reportOnUncaughtException = false;
}
// Add an auxiliary callback that coexists with the primary callback.
// Auxiliary callbacks are called first; if any returns true, the error is handled.
// Otherwise, the primary callback (if set) is called.
function addUncaughtExceptionCaptureCallback(fn) {
validateFunction(fn, 'fn');
if (exceptionHandlerState.auxiliaryCallbacks.length === 0 &&
exceptionHandlerState.captureFn === null) {
exceptionHandlerState.reportFlag =
process.report.reportOnUncaughtException === true;
process.report.reportOnUncaughtException = false;
shouldAbortOnUncaughtToggle[0] = 0;
}
ArrayPrototypePush(exceptionHandlerState.auxiliaryCallbacks, fn);
}
function hasUncaughtExceptionCaptureCallback() {
return exceptionHandlerState.captureFn !== null;
}
@@ -154,21 +174,33 @@ function createOnGlobalUncaughtException() {
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
process.emit('uncaughtExceptionMonitor', er, type);
// Primary callback (e.g., domain) has priority and always handles the exception
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
} else if (!process.emit('uncaughtException', er, type)) {
// If someone handled it, then great. Otherwise, die in C++ land
// since that means that we'll exit the process, emit the 'exit' event.
try {
if (!process._exiting) {
process._exiting = true;
process.exitCode = kGenericUserError;
process.emit('exit', kGenericUserError);
} else {
// If no primary callback, try auxiliary callbacks (e.g., REPL)
// They must return true to indicate handling
let handled = false;
for (let i = exceptionHandlerState.auxiliaryCallbacks.length - 1; i >= 0; i--) {
if (exceptionHandlerState.auxiliaryCallbacks[i](er) === true) {
handled = true;
break;
}
} catch {
// Nothing to be done about it at this point.
}
return false;
if (!handled && !process.emit('uncaughtException', er, type)) {
// If someone handled it, then great. Otherwise, die in C++ land
// since that means that we'll exit the process, emit the 'exit' event.
try {
if (!process._exiting) {
process._exiting = true;
process.exitCode = kGenericUserError;
process.emit('exit', kGenericUserError);
}
} catch {
// Nothing to be done about it at this point.
}
return false;
}
}
// If we handled an error, then make sure any ticks get processed
@@ -477,5 +509,6 @@ module.exports = {
evalScript,
onGlobalUncaughtException: createOnGlobalUncaughtException(),
setUncaughtExceptionCaptureCallback,
addUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback,
};
+183 -143
View File
@@ -73,7 +73,6 @@ const {
RegExpPrototypeExec,
SafePromiseRace,
SafeSet,
SafeWeakSet,
StringPrototypeCharAt,
StringPrototypeEndsWith,
StringPrototypeIncludes,
@@ -115,7 +114,7 @@ const {
const { Console } = require('console');
const { shouldColorize } = require('internal/util/colors');
const CJSModule = require('internal/modules/cjs/loader').Module;
const domain = require('domain');
const { AsyncLocalStorage } = require('async_hooks');
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
debug = fn;
});
@@ -123,6 +122,7 @@ const {
ErrorPrepareStackTrace,
codes: {
ERR_CANNOT_WATCH_SIGINT,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_REPL_EVAL_CONFIG,
ERR_INVALID_REPL_INPUT,
ERR_MISSING_ARGS,
@@ -178,22 +178,47 @@ const {
let processTopLevelAwait;
const parentModule = module;
const domainSet = new SafeWeakSet();
// AsyncLocalStorage to track which REPL instance owns the current async context
// This replaces the domain-based tracking for error handling
const replContext = new AsyncLocalStorage();
let exceptionCaptureSetup = false;
/**
* Sets up the uncaught exception capture callback to route errors
* to the appropriate REPL instance. This replaces domain-based error handling.
* Uses addUncaughtExceptionCaptureCallback to coexist with the primary
* callback (e.g., domain module).
*/
function setupExceptionCapture() {
if (exceptionCaptureSetup) return;
process.addUncaughtExceptionCaptureCallback((err) => {
const store = replContext.getStore();
if (store?.replServer && !store.replServer.closed) {
store.replServer._handleError(err);
return true; // We handled it
}
// No active REPL context - let other handlers try
});
exceptionCaptureSetup = true;
}
const kBufferedCommandSymbol = Symbol('bufferedCommand');
const kLoadingSymbol = Symbol('loading');
function processNewListener(event, listener) {
if (event === 'uncaughtException' &&
process.domain &&
listener.name !== 'domainUncaughtExceptionClear' &&
domainSet.has(process.domain)) {
// Throw an error so that the event will not be added and the current
// domain takes over. That way the user is notified about the error
// and the current code evaluation is stopped, just as any other code
// that contains an error.
throw new ERR_INVALID_REPL_INPUT(
'Listeners for `uncaughtException` cannot be used in the REPL');
if (event === 'uncaughtException') {
const store = replContext.getStore();
if (store?.replServer) {
// Throw an error so that the event will not be added and the
// current REPL handles it. That way the user is notified about
// the error and the current code evaluation is stopped, just as
// any other code that contains an error.
throw new ERR_INVALID_REPL_INPUT(
'Listeners for `uncaughtException` cannot be used in the REPL');
}
}
}
@@ -341,7 +366,13 @@ class REPLServer extends Interface {
this.allowBlockingCompletions = !!options.allowBlockingCompletions;
this.useColors = !!options.useColors;
this._domain = options.domain || domain.create();
this._isStandalone = !!options[kStandaloneREPL];
if (options.domain !== undefined) {
throw new ERR_INVALID_ARG_VALUE('options.domain', options.domain,
'is no longer supported');
}
this.useGlobal = !!useGlobal;
this.ignoreUndefined = !!ignoreUndefined;
this.replMode = replMode || module.exports.REPL_MODE_SLOPPY;
@@ -370,7 +401,8 @@ class REPLServer extends Interface {
this.once('exit', removeProcessNewListener);
}
domainSet.add(this._domain);
// Set up exception capture for async error handling
setupExceptionCapture();
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
const sep = '\u0000\u0000\u0000';
@@ -591,13 +623,16 @@ class REPLServer extends Interface {
}
} catch (e) {
err = e;
if (process.domain) {
debug('not recoverable, send to domain');
// If there's an active domain with error listeners, let it handle the error
if (process.domain?.listenerCount('error') > 0) {
debug('domain handling error');
process.domain.emit('error', err);
process.domain.exit();
return;
}
// Handle non-recoverable errors directly
debug('not recoverable, handle error');
self._handleError(err);
return;
}
if (awaitPromise && !err) {
@@ -623,13 +658,15 @@ class REPLServer extends Interface {
const result = (await promise)?.value;
finishExecution(null, result);
} catch (err) {
if (err && process.domain) {
debug('not recoverable, send to domain');
// If there's an active domain with error listeners, let it handle the error
if (process.domain?.listenerCount('error') > 0) {
debug('domain handling async error');
process.domain.emit('error', err);
process.domain.exit();
return;
} else {
// Handle non-recoverable async errors directly
debug('not recoverable, handle error');
self._handleError(err);
}
finishExecution(err);
} finally {
// Remove prioritized SIGINT listener if it was not called.
prioritizedSigintQueue.delete(sigintListener);
@@ -644,124 +681,16 @@ class REPLServer extends Interface {
}
}
self.eval = self._domain.bind(eval_);
self._domain.on('error', function debugDomainError(e) {
debug('domain error');
let errStack = '';
if (typeof e === 'object' && e !== null) {
overrideStackTrace.set(e, (error, stackFrames) => {
let frames;
if (typeof stackFrames === 'object') {
// Search from the bottom of the call stack to
// find the first frame with a null function name
const idx = ArrayPrototypeFindLastIndex(
stackFrames,
(frame) => frame.getFunctionName() === null,
);
// If found, get rid of it and everything below it
frames = ArrayPrototypeSlice(stackFrames, 0, idx);
} else {
frames = stackFrames;
}
// FIXME(devsnek): this is inconsistent with the checks
// that the real prepareStackTrace dispatch uses in
// lib/internal/errors.js.
if (typeof MainContextError.prepareStackTrace === 'function') {
return MainContextError.prepareStackTrace(error, frames);
}
return ErrorPrepareStackTrace(error, frames);
});
decorateErrorStack(e);
if (e.domainThrown) {
delete e.domain;
delete e.domainThrown;
}
if (isError(e)) {
if (e.stack) {
if (e.name === 'SyntaxError') {
// Remove stack trace.
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/^\s+at\s.*\n?/gm,
SideEffectFreeRegExpPrototypeSymbolReplace(/^REPL\d+:\d+\r?\n/, e.stack, ''),
'');
const importErrorStr = 'Cannot use import statement outside a ' +
'module';
if (StringPrototypeIncludes(e.message, importErrorStr)) {
e.message = 'Cannot use import statement inside the Node.js ' +
'REPL, alternatively use dynamic import: ' + toDynamicImport(ArrayPrototypeAt(self.lines, -1));
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/SyntaxError:.*\n/,
e.stack,
`SyntaxError: ${e.message}\n`);
}
} else if (self.replMode === module.exports.REPL_MODE_STRICT) {
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/(\s+at\s+REPL\d+:)(\d+)/,
e.stack,
(_, pre, line) => pre + (line - 1),
);
}
}
errStack = self.writer(e);
// Remove one line error braces to keep the old style in place.
if (errStack[0] === '[' && errStack[errStack.length - 1] === ']') {
errStack = StringPrototypeSlice(errStack, 1, -1);
}
}
}
if (!self.underscoreErrAssigned) {
self.lastError = e;
}
if (options[kStandaloneREPL] &&
process.listenerCount('uncaughtException') !== 0) {
process.nextTick(() => {
process.emit('uncaughtException', e);
self.clearBufferedCommand();
self.lines.level = [];
if (!self.closed) {
self.displayPrompt();
}
});
} else {
if (errStack === '') {
errStack = self.writer(e);
}
const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack);
let matched = false;
errStack = '';
ArrayPrototypeForEach(lines, (line) => {
if (!matched &&
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
errStack += writer.options.breakLength >= line.length ?
`Uncaught ${line}` :
`Uncaught:\n${line}`;
matched = true;
} else {
errStack += line;
}
});
if (!matched) {
const ln = lines.length === 1 ? ' ' : ':\n';
errStack = `Uncaught${ln}${errStack}`;
}
// Normalize line endings.
errStack += StringPrototypeEndsWith(errStack, '\n') ? '' : '\n';
self.output.write(errStack);
self.clearBufferedCommand();
self.lines.level = [];
if (!self.closed) {
self.displayPrompt();
}
}
});
// Wrap eval to run within the REPL's async context for error tracking.
// The function names are needed for stack trace filtering - they must not
// be anonymous, but we can't use 'eval' as a name since it's reserved.
const originalEval = eval_;
// eslint-disable-next-line func-name-matching
self.eval = function REPLEval(code, context, file, cb) {
replContext.run({ replServer: self }, function REPLEvalInContext() {
originalEval(code, context, file, cb);
});
};
self.clearBufferedCommand();
@@ -925,7 +854,7 @@ class REPLServer extends Interface {
}
if (e) {
self._domain.emit('error', e.err || e);
self._handleError(e.err || e);
self[kLastCommandErrored] = true;
}
@@ -1051,6 +980,117 @@ class REPLServer extends Interface {
clearBufferedCommand() {
this[kBufferedCommandSymbol] = '';
}
_handleError(e) {
debug('handle error');
let errStack = '';
if (typeof e === 'object' && e !== null) {
overrideStackTrace.set(e, (error, stackFrames) => {
let frames;
if (typeof stackFrames === 'object') {
// Search from the bottom of the call stack to
// find the first frame with a null function name
const idx = ArrayPrototypeFindLastIndex(
stackFrames,
(frame) => frame.getFunctionName() === null,
);
// If found, get rid of it and everything below it
frames = ArrayPrototypeSlice(stackFrames, 0, idx);
} else {
frames = stackFrames;
}
// FIXME(devsnek): this is inconsistent with the checks
// that the real prepareStackTrace dispatch uses in
// lib/internal/errors.js.
if (typeof MainContextError.prepareStackTrace === 'function') {
return MainContextError.prepareStackTrace(error, frames);
}
return ErrorPrepareStackTrace(error, frames);
});
decorateErrorStack(e);
if (isError(e)) {
if (e.stack) {
if (e.name === 'SyntaxError') {
// Remove stack trace.
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/^\s+at\s.*\n?/gm,
SideEffectFreeRegExpPrototypeSymbolReplace(/^REPL\d+:\d+\r?\n/, e.stack, ''),
'');
const importErrorStr = 'Cannot use import statement outside a ' +
'module';
if (StringPrototypeIncludes(e.message, importErrorStr)) {
e.message = 'Cannot use import statement inside the Node.js ' +
'REPL, alternatively use dynamic import: ' + toDynamicImport(ArrayPrototypeAt(this.lines, -1));
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/SyntaxError:.*\n/,
e.stack,
`SyntaxError: ${e.message}\n`);
}
} else if (this.replMode === module.exports.REPL_MODE_STRICT) {
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/(\s+at\s+REPL\d+:)(\d+)/,
e.stack,
(_, pre, line) => pre + (line - 1),
);
}
}
errStack = this.writer(e);
// Remove one line error braces to keep the old style in place.
if (errStack[0] === '[' && errStack[errStack.length - 1] === ']') {
errStack = StringPrototypeSlice(errStack, 1, -1);
}
}
}
if (!this.underscoreErrAssigned) {
this.lastError = e;
}
if (this._isStandalone &&
process.listenerCount('uncaughtException') !== 0) {
process.nextTick(() => {
process.emit('uncaughtException', e);
this.clearBufferedCommand();
this.lines.level = [];
if (!this.closed) {
this.displayPrompt();
}
});
} else {
if (errStack === '') {
errStack = this.writer(e);
}
const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack);
let matched = false;
errStack = '';
ArrayPrototypeForEach(lines, (line) => {
if (!matched &&
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
errStack += writer.options.breakLength >= line.length ?
`Uncaught ${line}` :
`Uncaught:\n${line}`;
matched = true;
} else {
errStack += line;
}
});
if (!matched) {
const ln = lines.length === 1 ? ' ' : ':\n';
errStack = `Uncaught${ln}${errStack}`;
}
// Normalize line endings.
errStack += StringPrototypeEndsWith(errStack, '\n') ? '' : '\n';
this.output.write(errStack);
this.clearBufferedCommand();
this.lines.level = [];
if (!this.closed) {
this.displayPrompt();
}
}
}
close() {
if (this.terminal && this.historyManager?.isFlushing && !this._closingOnFlush) {
this._closingOnFlush = true;
+1 -7
View File
@@ -2,9 +2,8 @@
const ArrayStream = require('../common/arraystream');
const repl = require('node:repl');
const assert = require('node:assert');
function startNewREPLServer(replOpts = {}, testingOpts = {}) {
function startNewREPLServer(replOpts = {}) {
const input = new ArrayStream();
const output = new ArrayStream();
@@ -20,11 +19,6 @@ function startNewREPLServer(replOpts = {}, testingOpts = {}) {
...replOpts,
});
if (!testingOpts.disableDomainErrorAssert) {
// Some errors are passed to the domain, but do not callback
replServer._domain.on('error', assert.ifError);
}
return { replServer, input, output };
}
+1 -7
View File
@@ -1,6 +1,5 @@
// Tab completion sometimes uses a separate REPL instance under the hood.
// That REPL instance has its own domain. Make sure domain errors trickle back
// up to the main REPL.
// Make sure errors in completion callbacks are properly thrown.
//
// Ref: https://github.com/nodejs/node/issues/21586
@@ -31,11 +30,6 @@ const repl = require('repl');
const putIn = new ArrayStream();
const testMe = repl.start('', putIn);
// Some errors are passed to the domain, but do not callback.
testMe._domain.on('error', function(err) {
throw err;
});
// Nesting of structures causes REPL to use a nested REPL for completion.
putIn.run([
'var top = function() {',
@@ -1,17 +1,22 @@
'use strict';
// Tests that domain can be loaded after setUncaughtExceptionCaptureCallback
// has been called. This verifies that the mutual exclusivity has been removed.
const common = require('../common');
const assert = require('assert');
// Set up a capture callback first
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
assert.throws(
() => require('domain'),
{
code: 'ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
name: 'Error',
message: /^A callback was registered.*with using the `domain` module/
}
);
// Loading domain should not throw (coexistence is now supported)
const domain = require('domain');
// Verify domain module loaded successfully
const assert = require('assert');
assert.ok(domain);
assert.ok(domain.create);
// Clean up
process.setUncaughtExceptionCaptureCallback(null);
require('domain'); // Should not throw.
// Domain should still be usable
const d = domain.create();
assert.ok(d);
@@ -1,30 +1,23 @@
'use strict';
// Tests that setUncaughtExceptionCaptureCallback can be called after domain
// is loaded. This verifies that the mutual exclusivity has been removed.
const common = require('../common');
const assert = require('assert');
Error.stackTraceLimit = Infinity;
// Load domain first
const domain = require('domain');
assert.ok(domain);
(function foobar() {
require('domain');
})();
// Setting callback should not throw (coexistence is now supported)
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
assert.throws(
() => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()),
(err) => {
common.expectsError(
{
code: 'ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
name: 'Error',
message: /^The `domain` module is in use, which is mutually/
}
)(err);
// Verify callback is registered
assert.ok(process.hasUncaughtExceptionCaptureCallback());
assert(err.stack.includes('-'.repeat(40)),
`expected ${err.stack} to contain dashes`);
// Clean up
process.setUncaughtExceptionCaptureCallback(null);
assert.ok(!process.hasUncaughtExceptionCaptureCallback());
const location = `at foobar (${__filename}:`;
assert(err.stack.includes(location),
`expected ${err.stack} to contain ${location}`);
return true;
}
);
// Domain should still be usable after callback operations
const d = domain.create();
assert.ok(d);
@@ -20,8 +20,6 @@ const assert = require('node:assert');
eval$.resolve();
});
},
}, {
disableDomainErrorAssert: true,
});
replServer.write('\n');
+1 -1
View File
@@ -3,5 +3,5 @@ require('../common');
const { startNewREPLServer } = require('../common/repl');
// Regression test for https://github.com/nodejs/node/issues/6802
const { input } = startNewREPLServer({ useGlobal: true }, { disableDomainErrorAssert: true });
const { input } = startNewREPLServer({ useGlobal: true });
input.run(['let process']);
+2 -2
View File
@@ -30,8 +30,8 @@ function testSloppyMode() {
}
function testStrictMode() {
const { input, output } = startNewREPLServer({ replMode: repl.REPL_MODE_STRICT, terminal: false, prompt: '> ' }, {
disableDomainErrorAssert: true,
const { input, output } = startNewREPLServer({
replMode: repl.REPL_MODE_STRICT, terminal: false, prompt: '> '
});
input.emit('data', 'x = 3\n');
@@ -0,0 +1,69 @@
'use strict';
// This test verifies that when multiple REPL instances exist concurrently,
// async errors are correctly routed to the REPL instance that created them.
const common = require('../common');
const assert = require('assert');
const repl = require('repl');
const { Writable, PassThrough } = require('stream');
// Create two REPLs with separate inputs and outputs
let output1 = '';
let output2 = '';
const input1 = new PassThrough();
const input2 = new PassThrough();
const writable1 = new Writable({
write(chunk, encoding, callback) {
output1 += chunk.toString();
callback();
}
});
const writable2 = new Writable({
write(chunk, encoding, callback) {
output2 += chunk.toString();
callback();
}
});
const r1 = repl.start({
input: input1,
output: writable1,
terminal: false,
prompt: 'R1> ',
});
const r2 = repl.start({
input: input2,
output: writable2,
terminal: false,
prompt: 'R2> ',
});
// Create async error in REPL 1
input1.write('setTimeout(() => { throw new Error("error from repl1") }, 10)\n');
// Create async error in REPL 2
input2.write('setTimeout(() => { throw new Error("error from repl2") }, 20)\n');
setTimeout(common.mustCall(() => {
r1.close();
r2.close();
// Verify error from REPL 1 went to REPL 1's output
assert.match(output1, /error from repl1/,
'REPL 1 should have received its own async error');
// Verify error from REPL 2 went to REPL 2's output
assert.match(output2, /error from repl2/,
'REPL 2 should have received its own async error');
// Verify errors did not cross over to wrong REPL
assert.doesNotMatch(output1, /error from repl2/,
'REPL 1 should not have received REPL 2\'s error');
assert.doesNotMatch(output2, /error from repl1/,
'REPL 2 should not have received REPL 1\'s error');
}), 100);
@@ -10,8 +10,6 @@ function run({ command, expected }) {
const { replServer, output } = startNewREPLServer({
terminal: false,
useColors: false
}, {
disableDomainErrorAssert: true,
});
replServer.write(`${command}\n`);
@@ -5,10 +5,7 @@ const { startNewREPLServer } = require('../common/repl');
const testingReplPrompt = '_REPL_TESTING_PROMPT_>';
const { replServer, output } = startNewREPLServer(
{ prompt: testingReplPrompt },
{ disableDomainErrorAssert: true }
);
const { replServer, output } = startNewREPLServer({ prompt: testingReplPrompt });
replServer.write('throw new Error("foo[a]")\n');
+5 -8
View File
@@ -7,14 +7,11 @@ const { startNewREPLServer } = require('../common/repl');
const stackRegExp = /(at .*REPL\d+:)[0-9]+:[0-9]+/g;
function run({ command, expected, ...extraREPLOptions }, i) {
const { replServer, output } = startNewREPLServer(
{
terminal: false,
useColors: false,
...extraREPLOptions
},
{ disableDomainErrorAssert: true }
);
const { replServer, output } = startNewREPLServer({
terminal: false,
useColors: false,
...extraREPLOptions
});
replServer.write(`${command}\n`);
if (typeof expected === 'string') {
+1 -3
View File
@@ -6,9 +6,7 @@ const { startNewREPLServer } = require('../common/repl');
common.skipIfInspectorDisabled();
const { input, output } = startNewREPLServer(
{ useColors: true }, { disableDomainErrorAssert: true }
);
const { input, output } = startNewREPLServer({ useColors: true });
output.accumulator = '';
@@ -11,7 +11,7 @@ process.on('exit', () => {
assert.strictEqual(found, true);
});
const { input, output } = startNewREPLServer({}, { disableDomainErrorAssert: true });
const { input, output } = startNewREPLServer();
output.write = (data) => {
// Matching only on a minimal piece of the stack because the string will vary
@@ -4,7 +4,7 @@ const common = require('../common');
const assert = require('assert');
const { startNewREPLServer } = require('../common/repl');
const { replServer, input } = startNewREPLServer({}, { disableDomainErrorAssert: true });
const { replServer, input } = startNewREPLServer();
// https://github.com/nodejs/node/issues/3346
// Tab-completion should be empty
-2
View File
@@ -10,6 +10,4 @@ const testMe = repl.start('', putIn, function(cmd, context, filename,
callback(null, cmd);
});
testMe._domain.on('error', common.mustNotCall());
testMe.complete('', common.mustSucceed());
+1 -1
View File
@@ -180,7 +180,7 @@ async function ordinaryTests() {
['k', '234'],
['const k = await Promise.resolve(345)', "Uncaught SyntaxError: Identifier 'k' has already been declared"],
// Regression test for https://github.com/nodejs/node/issues/43777.
['await Promise.resolve(123), Promise.resolve(456)', 'Promise {', { line: 0 }],
['await Promise.resolve(123), Promise.resolve(456)', 'Promise { 456 }'],
['await Promise.resolve(123), await Promise.resolve(456)', '456'],
['await (Promise.resolve(123), Promise.resolve(456))', '456'],
];
@@ -8,17 +8,12 @@ const common = require('../common');
const assert = require('assert');
const { startNewREPLServer } = require('../common/repl');
const { replServer, output } = startNewREPLServer(
{
prompt: '',
terminal: false,
useColors: false,
global: false,
},
{
disableDomainErrorAssert: true
},
);
const { replServer, output } = startNewREPLServer({
prompt: '',
terminal: false,
useColors: false,
global: false,
});
replServer.write(
'process.nextTick(() => {\n' +
@@ -3,21 +3,16 @@ const common = require('../common');
const assert = require('assert');
const { startNewREPLServer } = require('../common/repl');
const { replServer, output } = startNewREPLServer(
{
prompt: '',
terminal: false,
useColors: false,
global: false,
eval: common.mustCall((code, context, filename, cb) => {
replServer.setPrompt('prompt! ');
cb(new Error('err'));
})
},
{
disableDomainErrorAssert: true
},
);
const { replServer, output } = startNewREPLServer({
prompt: '',
terminal: false,
useColors: false,
global: false,
eval: common.mustCall((code, context, filename, cb) => {
replServer.setPrompt('prompt! ');
cb(new Error('err'));
})
});
replServer.write('foo\n');
+5 -10
View File
@@ -6,16 +6,11 @@ const { startNewREPLServer } = require('../common/repl');
let count = 0;
function run({ command, expected, useColors = false }) {
const { replServer, output } = startNewREPLServer(
{
prompt: '',
terminal: false,
useColors,
},
{
disableDomainErrorAssert: true
},
);
const { replServer, output } = startNewREPLServer({
prompt: '',
terminal: false,
useColors,
});
replServer.write(`${command}\n`);
-2
View File
@@ -138,8 +138,6 @@ function testError() {
prompt: testingReplPrompt,
replMode: repl.REPL_MODE_STRICT,
preview: false,
}, {
disableDomainErrorAssert: true
});
replServer.write(`_error; // initial value undefined
+1 -1
View File
@@ -8,7 +8,7 @@ const { startNewREPLServer } = require('../common/repl');
const cpuUsage = process.cpuUsage();
const { replServer } = startNewREPLServer({}, { disableDomainErrorAssert: true });
const { replServer } = startNewREPLServer();
replServer.input.emit('data', '{}');
replServer.input.emit('keypress', '', { name: 'left' });
replServer.input.emit('data', 'node');