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 <[email protected]>
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: Colin Ihrig <[email protected]>
Reviewed-By: Rich Trott <[email protected]>
  • Loading branch information
Flarna authored and MylesBorins committed Jan 16, 2020
1 parent deff600 commit 79eba6a
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
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');
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;

0 comments on commit 79eba6a

Please sign in to comment.