Skip to content
Permalink
Browse files

process: allow monitoring uncaughtException

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 Trott committed Jan 8, 2020
1 parent 6d44308 commit f4797ff1ef7304659d747d181ec1e7afac408d50
@@ -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
@@ -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
@@ -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)) {
@@ -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');
@@ -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');
@@ -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 f4797ff

Please sign in to comment.
You can’t perform that action at this time.