Skip to content

Commit 689a74c

Browse files
Flarnatargos
authored andcommitted
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: nodejs#31257 Refs: nodejs#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>
1 parent 13fbe01 commit 689a74c

File tree

5 files changed

+123
-0
lines changed

5 files changed

+123
-0
lines changed

doc/api/process.md

+32
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ nonexistentFunc();
262262
console.log('This will not run.');
263263
```
264264

265+
It is possible to monitor `'uncaughtException'` events without overriding the
266+
default behavior to exit the process by installing a
267+
`'uncaughtExceptionMonitor'` listener.
268+
265269
#### Warning: Using `'uncaughtException'` correctly
266270

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

296+
### Event: `'uncaughtExceptionMonitor'`
297+
<!-- YAML
298+
added: REPLACEME
299+
-->
300+
301+
* `err` {Error} The uncaught exception.
302+
* `origin` {string} Indicates if the exception originates from an unhandled
303+
rejection or from synchronous errors. Can either be `'uncaughtException'` or
304+
`'unhandledRejection'`.
305+
306+
The `'uncaughtExceptionMonitor'` event is emitted before an
307+
`'uncaughtException'` event is emitted or a hook installed via
308+
[`process.setUncaughtExceptionCaptureCallback()`][] is called.
309+
310+
Installing an `'uncaughtExceptionMonitor'` listener does not change the behavior
311+
once an `'uncaughtException'` event is emitted. The process will
312+
still crash if no `'uncaughtException'` listener is installed.
313+
314+
```js
315+
process.on('uncaughtExceptionMonitor', (err, origin) => {
316+
MyMonitoringTool.logSync(err, origin);
317+
});
318+
319+
// Intentionally cause an exception, but don't catch it.
320+
nonexistentFunc();
321+
// Still crashes Node.js
322+
```
323+
292324
### Event: `'unhandledRejection'`
293325
<!-- YAML
294326
added: v1.4.1

lib/internal/process/execution.js

+1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ function createOnGlobalUncaughtException() {
159159
}
160160

161161
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
162+
process.emit('uncaughtExceptionMonitor', er, type);
162163
if (exceptionHandlerState.captureFn !== null) {
163164
exceptionHandlerState.captureFn(er);
164165
} else if (!process.emit('uncaughtException', er, type)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
3+
// Keep the event loop alive.
4+
setTimeout(() => {}, 1e6);
5+
6+
process.on('uncaughtExceptionMonitor', (err) => {
7+
console.log(`Monitored: ${err.message}`);
8+
});
9+
10+
throw new Error('Shall exit');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
// Keep the event loop alive.
4+
setTimeout(() => {}, 1e6);
5+
6+
process.on('uncaughtExceptionMonitor', (err) => {
7+
console.log(`Monitored: ${err.message}, will throw now`);
8+
missingFunction();
9+
});
10+
11+
throw new Error('Shall exit');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { execFile } = require('child_process');
6+
const fixtures = require('../common/fixtures');
7+
8+
{
9+
// Verify exit behavior is unchanged
10+
const fixture = fixtures.path('uncaught-exceptions', 'uncaught-monitor1.js');
11+
execFile(
12+
process.execPath,
13+
[fixture],
14+
common.mustCall((err, stdout, stderr) => {
15+
assert.strictEqual(err.code, 1);
16+
assert.strictEqual(Object.getPrototypeOf(err).name, 'Error');
17+
assert.strictEqual(stdout, 'Monitored: Shall exit\n');
18+
const errLines = stderr.trim().split(/[\r\n]+/);
19+
const errLine = errLines.find((l) => /^Error/.exec(l));
20+
assert.strictEqual(errLine, 'Error: Shall exit');
21+
})
22+
);
23+
}
24+
25+
{
26+
// Verify exit behavior is unchanged
27+
const fixture = fixtures.path('uncaught-exceptions', 'uncaught-monitor2.js');
28+
execFile(
29+
process.execPath,
30+
[fixture],
31+
common.mustCall((err, stdout, stderr) => {
32+
assert.strictEqual(err.code, 7);
33+
assert.strictEqual(Object.getPrototypeOf(err).name, 'Error');
34+
assert.strictEqual(stdout, 'Monitored: Shall exit, will throw now\n');
35+
const errLines = stderr.trim().split(/[\r\n]+/);
36+
const errLine = errLines.find((l) => /^ReferenceError/.exec(l));
37+
assert.strictEqual(
38+
errLine,
39+
'ReferenceError: missingFunction is not defined'
40+
);
41+
})
42+
);
43+
}
44+
45+
const theErr = new Error('MyError');
46+
47+
process.on(
48+
'uncaughtExceptionMonitor',
49+
common.mustCall((err, origin) => {
50+
assert.strictEqual(err, theErr);
51+
assert.strictEqual(origin, 'uncaughtException');
52+
}, 2)
53+
);
54+
55+
process.on('uncaughtException', common.mustCall((err, origin) => {
56+
assert.strictEqual(origin, 'uncaughtException');
57+
assert.strictEqual(err, theErr);
58+
}));
59+
60+
process.nextTick(common.mustCall(() => {
61+
// Test with uncaughtExceptionCaptureCallback installed
62+
process.setUncaughtExceptionCaptureCallback(common.mustCall(
63+
(err) => assert.strictEqual(err, theErr))
64+
);
65+
66+
throw theErr;
67+
}));
68+
69+
throw theErr;

0 commit comments

Comments
 (0)