Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,42 @@ generate a core file.

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

## `process.addUncaughtExceptionCaptureCallback(fn)`

<!-- YAML
added: REPLACEME
-->

* `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
Expand Down Expand Up @@ -4038,8 +4074,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`

Expand Down Expand Up @@ -4545,6 +4581,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
Expand Down
20 changes: 2 additions & 18 deletions lib/domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ ObjectDefineProperty(process, 'features', {
const {
onGlobalUncaughtException,
setUncaughtExceptionCaptureCallback,
addUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');

Expand All @@ -318,6 +319,8 @@ ObjectDefineProperty(process, 'features', {
process._fatalException = onGlobalUncaughtException;
process.setUncaughtExceptionCaptureCallback =
setUncaughtExceptionCaptureCallback;
process.addUncaughtExceptionCaptureCallback =
addUncaughtExceptionCaptureCallback;
process.hasUncaughtExceptionCaptureCallback =
hasUncaughtExceptionCaptureCallback;
}
Expand Down
44 changes: 39 additions & 5 deletions lib/internal/process/execution.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayPrototypePush,
RegExpPrototypeExec,
StringPrototypeIndexOf,
StringPrototypeSlice,
Expand Down Expand Up @@ -105,15 +106,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') {
Expand All @@ -129,6 +133,23 @@ 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) {
if (typeof fn !== 'function') {
throw new ERR_INVALID_ARG_TYPE('fn', 'Function', 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;
}
Expand All @@ -154,9 +175,21 @@ function createOnGlobalUncaughtException() {

const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
process.emit('uncaughtExceptionMonitor', er, type);
let handled = false;
// Primary callback (e.g., domain) has priority - it handles domain-specific errors
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
} else if (!process.emit('uncaughtException', er, type)) {
handled = exceptionHandlerState.captureFn(er);
}
// If primary didn't handle it, try auxiliary callbacks (e.g., REPL)
if (!handled) {
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 {
Expand Down Expand Up @@ -473,5 +506,6 @@ module.exports = {
evalScript,
onGlobalUncaughtException: createOnGlobalUncaughtException(),
setUncaughtExceptionCaptureCallback,
addUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback,
};
Loading
Loading