Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

process: allow monitoring uncaughtException #31257

Closed
28 changes: 28 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,20 @@ nonexistentFunc();
console.log('This will not run.');
```

It is possible to monitor `'uncaughtException'` events without overriding the
default behavior to exit the process by installing a listener using the symbol
`uncaughtExceptionMonitor`.

```js
process.on(process.uncaughtExceptionMonitor, (err, origin) => {
Flarna marked this conversation as resolved.
Show resolved Hide resolved
MyMonitoringTool.logSync(err, origin);
});

// Intentionally cause an exception, but don't catch it.
nonexistentFunc();
// Still crashes Node.js
```

#### Warning: Using `'uncaughtException'` correctly

`'uncaughtException'` is a crude mechanism for exception handling
Expand Down Expand Up @@ -2320,6 +2334,20 @@ documentation for the [`'warning'` event][process_warning] and the
[`emitWarning()` method][process_emit_warning] for more information about this
flag's behavior.

## `process.uncaughtExceptionMonitor`
<!-- YAML
added: REPLACEME
-->

This symbol shall be used to install a listener for only monitoring
`'uncaughtException'` events. Listeners installed using this symbol are called
before the regular `'uncaughtException'` listeners and before a hook
installed via [`process.setUncaughtExceptionCaptureCallback()`][].

Installing a listener using this symbol does not change the behavior once an
`'uncaughtException'` event is emitted, therefore the process will still crash
if no regular `'uncaughtException'` listener is installed.

## `process.umask([mask])`
<!-- YAML
added: v0.1.19
Expand Down
9 changes: 8 additions & 1 deletion lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ ObjectDefineProperty(process, 'features', {
const {
onGlobalUncaughtException,
setUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback
hasUncaughtExceptionCaptureCallback,
kUncaughtExceptionMonitor
} = require('internal/process/execution');

// For legacy reasons this is still called `_fatalException`, even
Expand All @@ -220,6 +221,12 @@ ObjectDefineProperty(process, 'features', {
setUncaughtExceptionCaptureCallback;
process.hasUncaughtExceptionCaptureCallback =
hasUncaughtExceptionCaptureCallback;
ObjectDefineProperty(process, 'uncaughtExceptionMonitor', {
value: kUncaughtExceptionMonitor,
writable: false,
configurable: true,
enumerable: true
});
}

const { emitWarning } = require('internal/process/warning');
Expand Down
7 changes: 6 additions & 1 deletion lib/internal/process/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const {
JSONStringify,
PromiseResolve,
Symbol,
} = primordials;

const path = require('path');
Expand All @@ -27,6 +28,8 @@ const {
// communication with JS.
const { shouldAbortOnUncaughtToggle } = internalBinding('util');

const kUncaughtExceptionMonitor = Symbol('process.uncaughtExceptionMonitor');

function tryGetCwd() {
try {
return process.cwd();
Expand Down Expand Up @@ -159,6 +162,7 @@ function createOnGlobalUncaughtException() {
}

const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
process.emit(kUncaughtExceptionMonitor, er, type);
BridgeAR marked this conversation as resolved.
Show resolved Hide resolved
Flarna marked this conversation as resolved.
Show resolved Hide resolved
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
} else if (!process.emit('uncaughtException', er, type)) {
Expand Down Expand Up @@ -214,5 +218,6 @@ module.exports = {
evalScript,
onGlobalUncaughtException: createOnGlobalUncaughtException(),
setUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback
hasUncaughtExceptionCaptureCallback,
kUncaughtExceptionMonitor
};
10 changes: 10 additions & 0 deletions test/fixtures/uncaught-exceptions/uncaught-monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

// Keep the event loop alive.
setTimeout(() => {}, 1e6);

process.on(process.uncaughtExceptionMonitor, (err) => {
console.log(`Monitored: ${err.message}`);
});

Flarna marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('Shall exit');
49 changes: 49 additions & 0 deletions test/parallel/test-process-uncaught-exception-monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { execFile } = require('child_process');
const fixtures = require('../common/fixtures');

{
// Verify exit behavior is unchanged
const fixture = fixtures.path('uncaught-exceptions', 'uncaught-monitor.js');
execFile(
process.execPath,
[fixture],
common.mustCall((err, stdout, stderr) => {
assert.strictEqual(err.code, 1);
assert.strictEqual(Object.getPrototypeOf(err).name, 'Error');
assert.strictEqual(stdout, 'Monitored: Shall exit\n');
const errLines = stderr.trim().split(/[\r\n]+/);
const errLine = errLines.find((l) => /^Error/.exec(l));
assert.strictEqual(errLine, 'Error: Shall exit');
})
);
}

const theErr = new Error('MyError');

process.on(
process.uncaughtExceptionMonitor,
common.mustCall((err, origin) => {
assert.strictEqual(err, theErr);
assert.strictEqual(origin, 'uncaughtException');
}, 2)
);

process.on('uncaughtException', common.mustCall((err, origin) => {
assert.strictEqual(origin, 'uncaughtException');
assert.strictEqual(err, theErr);
}));

process.nextTick(common.mustCall(() => {
// Test with uncaughtExceptionCaptureCallback installed
process.setUncaughtExceptionCaptureCallback(common.mustCall(
(err) => assert.strictEqual(err, theErr))
);

throw theErr;
}));

throw theErr;