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)`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we land it as experimental first?


<!-- 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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't it be available on global process?

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should add this to changes: at the top of this function documentation

[`domain`][] built-in module.
To register multiple callbacks that can coexist, use
[`process.addUncaughtExceptionCaptureCallback()`][] instead.

## `process.sourceMapsEnabled`

Expand Down Expand Up @@ -4571,6 +4607,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 @@ -307,6 +307,7 @@ ObjectDefineProperty(process, 'features', {
const {
onGlobalUncaughtException,
setUncaughtExceptionCaptureCallback,
addUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');

Expand All @@ -319,6 +320,8 @@ ObjectDefineProperty(process, 'features', {
process._fatalException = onGlobalUncaughtException;
process.setUncaughtExceptionCaptureCallback =
setUncaughtExceptionCaptureCallback;
process.addUncaughtExceptionCaptureCallback =
addUncaughtExceptionCaptureCallback;
process.hasUncaughtExceptionCaptureCallback =
hasUncaughtExceptionCaptureCallback;
}
Expand Down
62 changes: 48 additions & 14 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new ERR_INVALID_ARG_TYPE('fn', 'Function', 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;
}
Expand All @@ -154,21 +175,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
Expand Down Expand Up @@ -477,5 +510,6 @@ module.exports = {
evalScript,
onGlobalUncaughtException: createOnGlobalUncaughtException(),
setUncaughtExceptionCaptureCallback,
addUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback,
};
Loading
Loading