diff --git a/doc/api/process.md b/doc/api/process.md index 7521cfe6fa4793..94186bcdcb3dd8 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -736,6 +736,44 @@ generate a core file. This feature is not available in [`Worker`][] threads. +## `process.addUncaughtExceptionCaptureCallback(fn)` + + + +> 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` * `fn` {Function|null} @@ -4038,8 +4081,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` @@ -4571,6 +4614,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 diff --git a/lib/domain.js b/lib/domain.js index 7dd16ee1bf59ef..f3a42271d2326d 100644 --- a/lib/domain.js +++ b/lib/domain.js @@ -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; diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 2fafdb92a78e04..af4027b7324c7b 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -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; } diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 9f484e6a611d33..4a2b57790bd7e6 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -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; + } + } + 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. } - } catch { - // Nothing to be done about it at this point. + return false; } - 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, }; diff --git a/lib/repl.js b/lib/repl.js index 5ad9e4fbb1506f..4b90a9a87a0c3e 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -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,7 +178,32 @@ 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'); @@ -313,7 +338,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; @@ -338,26 +369,27 @@ class REPLServer extends Interface { // from inside the REPL. This is useful for anyone working on the REPL. module.exports.repl = this; } else if (!addedNewListener) { - // Add this listener only once and use a WeakSet that contains the REPLs - // domains. Otherwise we'd have to add a single listener to each REPL - // instance and that could trigger the `MaxListenersExceededWarning`. + // Add this listener only once. Otherwise we'd have to add a single + // listener to each REPL instance and that could trigger the + // `MaxListenersExceededWarning`. process.prependListener('newListener', (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'); + } } }); addedNewListener = true; } - domainSet.add(this._domain); + // Set up exception capture for async error handling + setupExceptionCapture(); const savedRegExMatches = ['', '', '', '', '', '', '', '', '', '']; const sep = '\u0000\u0000\u0000'; @@ -578,13 +610,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) { @@ -610,13 +645,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); @@ -631,124 +668,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(); @@ -912,7 +841,7 @@ class REPLServer extends Interface { } if (e) { - self._domain.emit('error', e.err || e); + self._handleError(e.err || e); self[kLastCommandErrored] = true; } @@ -1038,6 +967,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; diff --git a/test/common/repl.js b/test/common/repl.js index 223050c76e1df0..6ce4d993a4c456 100644 --- a/test/common/repl.js +++ b/test/common/repl.js @@ -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 }; } diff --git a/test/fixtures/repl-tab-completion-nested-repls.js b/test/fixtures/repl-tab-completion-nested-repls.js index 1d2b154f2b3341..79677491eca55f 100644 --- a/test/fixtures/repl-tab-completion-nested-repls.js +++ b/test/fixtures/repl-tab-completion-nested-repls.js @@ -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() {', diff --git a/test/parallel/test-domain-load-after-set-uncaught-exception-capture.js b/test/parallel/test-domain-load-after-set-uncaught-exception-capture.js index 4018220711517f..73f5f989b8e776 100644 --- a/test/parallel/test-domain-load-after-set-uncaught-exception-capture.js +++ b/test/parallel/test-domain-load-after-set-uncaught-exception-capture.js @@ -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); diff --git a/test/parallel/test-domain-set-uncaught-exception-capture-after-load.js b/test/parallel/test-domain-set-uncaught-exception-capture-after-load.js index 4bf419d76eb453..64f129fd201781 100644 --- a/test/parallel/test-domain-set-uncaught-exception-capture-after-load.js +++ b/test/parallel/test-domain-set-uncaught-exception-capture-after-load.js @@ -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); diff --git a/test/parallel/test-repl-eval-error-after-close.js b/test/parallel/test-repl-eval-error-after-close.js index 8c30a533efba15..0b4683fda4bfda 100644 --- a/test/parallel/test-repl-eval-error-after-close.js +++ b/test/parallel/test-repl-eval-error-after-close.js @@ -20,8 +20,6 @@ const assert = require('node:assert'); eval$.resolve(); }); }, - }, { - disableDomainErrorAssert: true, }); replServer.write('\n'); diff --git a/test/parallel/test-repl-let-process.js b/test/parallel/test-repl-let-process.js index eb6cbc6a472c72..22b57ab5bb977e 100644 --- a/test/parallel/test-repl-let-process.js +++ b/test/parallel/test-repl-let-process.js @@ -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']); diff --git a/test/parallel/test-repl-mode.js b/test/parallel/test-repl-mode.js index 3faa0a96d58799..4d614af1a16392 100644 --- a/test/parallel/test-repl-mode.js +++ b/test/parallel/test-repl-mode.js @@ -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'); diff --git a/test/parallel/test-repl-multiple-instances-async-error.js b/test/parallel/test-repl-multiple-instances-async-error.js new file mode 100644 index 00000000000000..ddc8a5eaccdcca --- /dev/null +++ b/test/parallel/test-repl-multiple-instances-async-error.js @@ -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); diff --git a/test/parallel/test-repl-pretty-custom-stack.js b/test/parallel/test-repl-pretty-custom-stack.js index 82df8ff4fc6335..0efb814f38d2b5 100644 --- a/test/parallel/test-repl-pretty-custom-stack.js +++ b/test/parallel/test-repl-pretty-custom-stack.js @@ -10,8 +10,6 @@ function run({ command, expected }) { const { replServer, output } = startNewREPLServer({ terminal: false, useColors: false - }, { - disableDomainErrorAssert: true, }); replServer.write(`${command}\n`); diff --git a/test/parallel/test-repl-pretty-stack-custom-writer.js b/test/parallel/test-repl-pretty-stack-custom-writer.js index 2d39633030d775..e31460dbc93efb 100644 --- a/test/parallel/test-repl-pretty-stack-custom-writer.js +++ b/test/parallel/test-repl-pretty-stack-custom-writer.js @@ -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'); diff --git a/test/parallel/test-repl-pretty-stack.js b/test/parallel/test-repl-pretty-stack.js index a7f13dea75aaea..b2f9cc82c08df0 100644 --- a/test/parallel/test-repl-pretty-stack.js +++ b/test/parallel/test-repl-pretty-stack.js @@ -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') { diff --git a/test/parallel/test-repl-preview-newlines.js b/test/parallel/test-repl-preview-newlines.js index 22ffe0db108590..34a944beb538d7 100644 --- a/test/parallel/test-repl-preview-newlines.js +++ b/test/parallel/test-repl-preview-newlines.js @@ -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 = ''; diff --git a/test/parallel/test-repl-syntax-error-stack.js b/test/parallel/test-repl-syntax-error-stack.js index 1b6e3fb6e879f2..16bf27d045bc77 100644 --- a/test/parallel/test-repl-syntax-error-stack.js +++ b/test/parallel/test-repl-syntax-error-stack.js @@ -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 diff --git a/test/parallel/test-repl-tab-complete-crash.js b/test/parallel/test-repl-tab-complete-crash.js index 58628eb85b2a38..29f75028bdac94 100644 --- a/test/parallel/test-repl-tab-complete-crash.js +++ b/test/parallel/test-repl-tab-complete-crash.js @@ -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 diff --git a/test/parallel/test-repl-tab.js b/test/parallel/test-repl-tab.js index e99f667c4a38f5..710fca9fae2d1e 100644 --- a/test/parallel/test-repl-tab.js +++ b/test/parallel/test-repl-tab.js @@ -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()); diff --git a/test/parallel/test-repl-top-level-await.js b/test/parallel/test-repl-top-level-await.js index 0b35443dbdce14..a94ff8e48984a3 100644 --- a/test/parallel/test-repl-top-level-await.js +++ b/test/parallel/test-repl-top-level-await.js @@ -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'], ]; diff --git a/test/parallel/test-repl-uncaught-exception-async.js b/test/parallel/test-repl-uncaught-exception-async.js index f4180080f496f7..e5373cdaca4d8d 100644 --- a/test/parallel/test-repl-uncaught-exception-async.js +++ b/test/parallel/test-repl-uncaught-exception-async.js @@ -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' + diff --git a/test/parallel/test-repl-uncaught-exception-evalcallback.js b/test/parallel/test-repl-uncaught-exception-evalcallback.js index 77d03320ee9375..844fce6995aaa6 100644 --- a/test/parallel/test-repl-uncaught-exception-evalcallback.js +++ b/test/parallel/test-repl-uncaught-exception-evalcallback.js @@ -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'); diff --git a/test/parallel/test-repl-uncaught-exception.js b/test/parallel/test-repl-uncaught-exception.js index 7753fe180b07fd..012c7f59ebc8a8 100644 --- a/test/parallel/test-repl-uncaught-exception.js +++ b/test/parallel/test-repl-uncaught-exception.js @@ -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`); diff --git a/test/parallel/test-repl-underscore.js b/test/parallel/test-repl-underscore.js index 4c091a268fb701..c9ae7ca0e7ca0c 100644 --- a/test/parallel/test-repl-underscore.js +++ b/test/parallel/test-repl-underscore.js @@ -138,8 +138,6 @@ function testError() { prompt: testingReplPrompt, replMode: repl.REPL_MODE_STRICT, preview: false, - }, { - disableDomainErrorAssert: true }); replServer.write(`_error; // initial value undefined diff --git a/test/pummel/test-repl-paste-big-data.js b/test/pummel/test-repl-paste-big-data.js index 2265a1af8e393c..46f8415a2b0aae 100644 --- a/test/pummel/test-repl-paste-big-data.js +++ b/test/pummel/test-repl-paste-big-data.js @@ -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');