Skip to content

Commit

Permalink
process: allow monitoring uncaughtException
Browse files Browse the repository at this point in the history
Installing an uncaughtException listener has a side effect that process
is not aborted. This is quite bad for monitoring/logging tools which
tend to be interested in errors but don't want to cause side effects
like swallow an exception or change the output on console.

There are some workarounds in the wild like monkey patching emit or
rethrow in the exception if monitoring tool detects that it is the only
listener but this is error prone and risky.

This PR allows to install a listener to monitor uncaughtException
without the side effect to consider the exception has handled.

PR-URL: #31257
Refs: #30932
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
  • Loading branch information
Flarna authored and targos committed Apr 28, 2020
1 parent 9cf9cb4 commit de3603f
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 0 deletions.
32 changes: 32 additions & 0 deletions doc/api/process.md
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
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
@@ -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
@@ -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');
69 changes: 69 additions & 0 deletions test/parallel/test-process-uncaught-exception-monitor.js
@@ -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;

0 comments on commit de3603f

Please sign in to comment.