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
32 changes: 32 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ 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
`'uncaughtExceptionMonitor'` listener.

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

`'uncaughtException'` is a crude mechanism for exception handling
Expand Down Expand Up @@ -289,6 +293,34 @@ To restart a crashed application in a more reliable way, whether
in a separate process to detect application failures and recover or restart as
needed.

### Event: `'uncaughtExceptionMonitor'`
<!-- YAML
added: REPLACEME
-->

* `err` {Error} The uncaught exception.
* `origin` {string} Indicates if the exception originates from an unhandled
rejection or from synchronous errors. Can either be `'uncaughtException'` or
`'unhandledRejection'`.

The `'uncaughtExceptionMonitor'` event is emitted before an
`'uncaughtException'` event is emitted or a hook installed via
[`process.setUncaughtExceptionCaptureCallback()`][] is called.

Installing an `'uncaughtExceptionMonitor'` listener does not change the behavior
once an `'uncaughtException'` event is emitted. The process will
still crash if no `'uncaughtException'` listener is installed.

```js
process.on('uncaughtExceptionMonitor', (err, origin) => {
MyMonitoringTool.logSync(err, origin);
});

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

### Event: `'unhandledRejection'`
<!-- YAML
added: v1.4.1
Expand Down
1 change: 1 addition & 0 deletions lib/internal/process/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ function createOnGlobalUncaughtException() {
}

const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
process.emit('uncaughtExceptionMonitor', er, type);
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
} else if (!process.emit('uncaughtException', er, type)) {
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/uncaught-exceptions/uncaught-monitor1.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('uncaughtExceptionMonitor', (err) => {
console.log(`Monitored: ${err.message}`);
});

throw new Error('Shall exit');
11 changes: 11 additions & 0 deletions test/fixtures/uncaught-exceptions/uncaught-monitor2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

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

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

throw new Error('Shall exit');
Flarna marked this conversation as resolved.
Show resolved Hide resolved
69 changes: 69 additions & 0 deletions test/parallel/test-process-uncaught-exception-monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'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-monitor1.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');
})
);
}

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

const theErr = new Error('MyError');

process.on(
'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;