Skip to content

Commit 7082a27

Browse files
author
Julien Gilli
committed
process: allow multiple uncaught exception capture calbacks
1 parent 8884a98 commit 7082a27

16 files changed

+57
-139
lines changed

doc/api/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ greater than `4` (its current default value). For more information, see the
754754
[`--openssl-config`]: #cli_openssl_config_file
755755
[`Buffer`]: buffer.html#buffer_class_buffer
756756
[`SlowBuffer`]: buffer.html#buffer_class_slowbuffer
757-
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
757+
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_owner_fn
758758
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
759759
[REPL]: repl.html
760760
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage

doc/api/errors.md

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -803,23 +803,6 @@ A signing `key` was not provided to the [`sign.sign()`][] method.
803803

804804
`c-ares` failed to set the DNS server.
805805

806-
<a id="ERR_DOMAIN_CALLBACK_NOT_AVAILABLE"></a>
807-
### ERR_DOMAIN_CALLBACK_NOT_AVAILABLE
808-
809-
The `domain` module was not usable since it could not establish the required
810-
error handling hooks, because
811-
[`process.setUncaughtExceptionCaptureCallback()`][] had been called at an
812-
earlier point in time.
813-
814-
<a id="ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE"></a>
815-
### ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE
816-
817-
[`process.setUncaughtExceptionCaptureCallback()`][] could not be called
818-
because the `domain` module has been loaded at an earlier point in time.
819-
820-
The stack trace is extended to include the point in time at which the
821-
`domain` module had been loaded.
822-
823806
<a id="ERR_ENCODING_INVALID_ENCODED_DATA"></a>
824807
### ERR_ENCODING_INVALID_ENCODED_DATA
825808

@@ -1720,15 +1703,6 @@ A `Transform` stream finished with data still in the write buffer.
17201703

17211704
The initialization of a TTY failed due to a system error.
17221705

1723-
<a id="ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET"></a>
1724-
### ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET
1725-
1726-
[`process.setUncaughtExceptionCaptureCallback()`][] was called twice,
1727-
without first resetting the callback to `null`.
1728-
1729-
This error is designed to prevent accidentally overwriting a callback registered
1730-
from another module.
1731-
17321706
<a id="ERR_UNESCAPED_CHARACTERS"></a>
17331707
### ERR_UNESCAPED_CHARACTERS
17341708

@@ -2153,7 +2127,6 @@ such as `process.stdout.on('data')`.
21532127
[`new URL(input)`]: url.html#url_constructor_new_url_input_base
21542128
[`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable
21552129
[`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback
2156-
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
21572130
[`readable._read()`]: stream.html#stream_readable_read_size_1
21582131
[`require('crypto').setEngine()`]: crypto.html#crypto_crypto_setengine_engine_flags
21592132
[`require()`]: modules.html#modules_require

doc/api/process.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,11 +1807,12 @@ This function is only available on POSIX platforms (i.e. not Windows or
18071807
Android).
18081808
This feature is not available in [`Worker`][] threads.
18091809

1810-
## process.setUncaughtExceptionCaptureCallback(fn)
1810+
## process.setUncaughtExceptionCaptureCallback(owner, fn)
18111811
<!-- YAML
18121812
added: v9.3.0
18131813
-->
18141814

1815+
* `owner` {symbol}
18151816
* `fn` {Function|null}
18161817

18171818
The `process.setUncaughtExceptionCaptureCallback()` function sets a function
@@ -1824,12 +1825,7 @@ command line or set through [`v8.setFlagsFromString()`][], the process will
18241825
not abort.
18251826

18261827
To unset the capture function,
1827-
`process.setUncaughtExceptionCaptureCallback(null)` may be used. Calling this
1828-
method with a non-`null` argument while another capture function is set will
1829-
throw an error.
1830-
1831-
Using this function is mutually exclusive with using the deprecated
1832-
[`domain`][] built-in module.
1828+
`process.setUncaughtExceptionCaptureCallback(owner, null)` may be used.
18331829

18341830
## process.stderr
18351831

@@ -2137,7 +2133,6 @@ cases:
21372133
[`Worker`]: worker_threads.html#worker_threads_class_worker
21382134
[`console.error()`]: console.html#console_console_error_data_args
21392135
[`console.log()`]: console.html#console_console_log_data_args
2140-
[`domain`]: domain.html
21412136
[`net.Server`]: net.html#net_class_net_server
21422137
[`net.Socket`]: net.html#net_class_net_socket
21432138
[`NODE_OPTIONS`]: cli.html#cli_node_options_options
@@ -2150,7 +2145,7 @@ cases:
21502145
[`process.hrtime()`]: #process_process_hrtime_time
21512146
[`process.hrtime.bigint()`]: #process_process_hrtime_bigint
21522147
[`process.kill()`]: #process_process_kill_pid_signal
2153-
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
2148+
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_owner_fn
21542149
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
21552150
[`require()`]: globals.html#globals_require
21562151
[`require.main`]: modules.html#modules_accessing_the_main_module

doc/api/repl.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,8 @@ global or scoped variable, the input `fs` will be evaluated on-demand as
146146
The REPL uses the [`domain`][] module to catch all uncaught exceptions for that
147147
REPL session.
148148

149-
This use of the [`domain`][] module in the REPL has these side effects:
150-
151-
* Uncaught exceptions do not emit the [`'uncaughtException'`][] event.
152-
* Trying to use [`process.setUncaughtExceptionCaptureCallback()`][] throws
153-
an [`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`][] error.
149+
This use of the [`domain`][] module in the REPL has the side effects of making
150+
uncaught exceptions not emit the [`'uncaughtException'`][] event.
154151

155152
#### Assignment of the `_` (underscore) variable
156153
<!-- YAML
@@ -627,9 +624,7 @@ For an example of running a REPL instance over [curl(1)][], see:
627624

628625
[`'uncaughtException'`]: process.html#process_event_uncaughtexception
629626
[`--experimental-repl-await`]: cli.html#cli_experimental_repl_await
630-
[`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`]: errors.html#errors_err_domain_cannot_set_uncaught_exception_capture
631627
[`domain`]: domain.html
632-
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
633628
[`readline.InterfaceCompleter`]: readline.html#readline_use_of_the_completer_function
634629
[`readline.Interface`]: readline.html#readline_class_interface
635630
[`repl.ReplServer`]: #repl_class_replserver

lib/domain.js

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@
2929
const util = require('util');
3030
const EventEmitter = require('events');
3131
const {
32-
ERR_DOMAIN_CALLBACK_NOT_AVAILABLE,
33-
ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE,
3432
ERR_UNHANDLED_ERROR
3533
} = require('internal/errors').codes;
3634
const { createHook } = require('async_hooks');
3735

36+
const CAPTURE_CB_KEY = Symbol('domain');
37+
3838
// overwrite process.domain with a getter/setter that will allow for more
3939
// effective optimizations
4040
var _domain = [null];
@@ -74,23 +74,7 @@ const asyncHook = createHook({
7474
}
7575
});
7676

77-
// When domains are in use, they claim full ownership of the
78-
// uncaught exception capture callback.
79-
if (process.hasUncaughtExceptionCaptureCallback()) {
80-
throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE();
81-
}
82-
83-
// Get the stack trace at the point where `domain` was required.
84-
// eslint-disable-next-line no-restricted-syntax
85-
const domainRequireStack = new Error('require(`domain`) at this point').stack;
86-
8777
const { setUncaughtExceptionCaptureCallback } = process;
88-
process.setUncaughtExceptionCaptureCallback = function(fn) {
89-
const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE();
90-
err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack;
91-
throw err;
92-
};
93-
9478

9579
let sendMakeCallbackDeprecation = false;
9680
function emitMakeCallbackDeprecation() {
@@ -125,10 +109,10 @@ internalBinding('domain').enable(topLevelDomainCallback);
125109

126110
function updateExceptionCapture() {
127111
if (stack.every((domain) => domain.listenerCount('error') === 0)) {
128-
setUncaughtExceptionCaptureCallback(null);
112+
setUncaughtExceptionCaptureCallback(CAPTURE_CB_KEY, null);
129113
} else {
130-
setUncaughtExceptionCaptureCallback(null);
131-
setUncaughtExceptionCaptureCallback((er) => {
114+
setUncaughtExceptionCaptureCallback(CAPTURE_CB_KEY, null);
115+
setUncaughtExceptionCaptureCallback(CAPTURE_CB_KEY, (er) => {
132116
return process.domain._errorHandler(er);
133117
});
134118
}
@@ -211,7 +195,7 @@ Domain.prototype._errorHandler = function(er) {
211195
// Clear the uncaughtExceptionCaptureCallback so that we know that, even
212196
// if technically the top-level domain is still active, it would
213197
// be ok to abort on an uncaught exception at this point
214-
setUncaughtExceptionCaptureCallback(null);
198+
setUncaughtExceptionCaptureCallback(CAPTURE_CB_KEY, null);
215199
try {
216200
caught = this.emit('error', er);
217201
} finally {

lib/internal/bootstrap/node.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
_shouldAbortOnUncaughtToggle },
2727
{ internalBinding, NativeModule },
2828
triggerFatalException) {
29-
const exceptionHandlerState = { captureFn: null };
29+
const exceptionHandlerState = { captureFns: new Map() };
3030
const isMainThread = internalBinding('worker').threadId === 0;
3131

3232
function startup() {
@@ -579,8 +579,10 @@
579579
// call that threw and was never cleared. So clear it now.
580580
clearDefaultTriggerAsyncId();
581581

582-
if (exceptionHandlerState.captureFn !== null) {
583-
exceptionHandlerState.captureFn(er);
582+
if (exceptionHandlerState.captureFns.size !== 0) {
583+
for (const fn of exceptionHandlerState.captureFns.values()) {
584+
fn(er);
585+
}
584586
} else if (!process.emit('uncaughtException', er)) {
585587
// If someone handled it, then great. otherwise, die in C++ land
586588
// since that means that we'll exit the process, emit the 'exit' event.

lib/internal/errors.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -563,15 +563,6 @@ E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
563563
'Input buffers must have the same length', RangeError);
564564
E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]',
565565
Error);
566-
E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
567-
'A callback was registered through ' +
568-
'process.setUncaughtExceptionCaptureCallback(), which is mutually ' +
569-
'exclusive with using the `domain` module',
570-
Error);
571-
E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
572-
'The `domain` module is in use, which is mutually exclusive with calling ' +
573-
'process.setUncaughtExceptionCaptureCallback()',
574-
Error);
575566
E('ERR_ENCODING_INVALID_ENCODED_DATA',
576567
'The encoded data was not valid for encoding %s', TypeError);
577568
E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
@@ -885,10 +876,6 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
885876
E('ERR_TRANSFORM_WITH_LENGTH_0',
886877
'Calling transform done when writableState.length != 0', Error);
887878
E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
888-
E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
889-
'`process.setupUncaughtExceptionCapture()` was called while a capture ' +
890-
'callback was already active',
891-
Error);
892879
E('ERR_UNESCAPED_CHARACTERS', '%s contains unescaped characters', TypeError);
893880
E('ERR_UNHANDLED_ERROR',
894881
// Using a default argument here is important so the argument is not counted

lib/internal/process/main_thread_only.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ function setupChildProcessIpcChannel() {
171171
}
172172
}
173173

174+
process.domainsMap = new Map();
175+
174176
module.exports = {
175177
setupStdio,
176178
setupProcessMethods,

lib/internal/process/per_thread.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const {
1212
ERR_INVALID_ARG_TYPE,
1313
ERR_INVALID_OPT_VALUE,
1414
ERR_OUT_OF_RANGE,
15-
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET,
1615
ERR_UNKNOWN_SIGNAL
1716
}
1817
} = require('internal/errors');
@@ -213,24 +212,27 @@ function setupUncaughtExceptionCapture(exceptionHandlerState,
213212
// shouldAbortOnUncaughtToggle is a typed array for faster
214213
// communication with JS.
215214

216-
process.setUncaughtExceptionCaptureCallback = function(fn) {
215+
process.setUncaughtExceptionCaptureCallback = function(owner, fn) {
217216
if (fn === null) {
218-
exceptionHandlerState.captureFn = fn;
217+
exceptionHandlerState.captureFns.delete(owner);
219218
shouldAbortOnUncaughtToggle[0] = 1;
220219
return;
221220
}
221+
222222
if (typeof fn !== 'function') {
223223
throw new ERR_INVALID_ARG_TYPE('fn', ['Function', 'null'], fn);
224224
}
225-
if (exceptionHandlerState.captureFn !== null) {
226-
throw new ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET();
225+
226+
if (typeof owner !== 'symbol') {
227+
throw new ERR_INVALID_ARG_TYPE('owner', ['Symbol'], owner);
227228
}
228-
exceptionHandlerState.captureFn = fn;
229+
230+
exceptionHandlerState.captureFns.set(owner, fn);
229231
shouldAbortOnUncaughtToggle[0] = 0;
230232
};
231233

232234
process.hasUncaughtExceptionCaptureCallback = function() {
233-
return exceptionHandlerState.captureFn !== null;
235+
return exceptionHandlerState.captureFns.size !== 0;
234236
};
235237
}
236238

lib/repl.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,12 +348,24 @@ function REPLServer(prompt,
348348
}
349349
}
350350
} catch (e) {
351+
let forwardedToDomain = false;
351352
err = e;
352353

353354
if (process.domain) {
354355
debug('not recoverable, send to domain');
356+
forwardedToDomain = true;
355357
process.domain.emit('error', err);
356358
process.domain.exit();
359+
}
360+
361+
debug('domainsMap values:' + process.domainsMap.values());
362+
for (const d of process.domainsMap.values()) {
363+
forwardedToDomain = true;
364+
d.emit('error', err);
365+
d.exit();
366+
}
367+
368+
if (forwardedToDomain) {
357369
return;
358370
}
359371
}
Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
'use strict';
22
const common = require('../common');
3+
const captureSym = Symbol('foo');
34

4-
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
5-
6-
common.expectsError(
7-
() => require('domain'),
8-
{
9-
code: 'ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
10-
type: Error,
11-
message: /^A callback was registered.*with using the `domain` module/
12-
}
13-
);
14-
15-
process.setUncaughtExceptionCaptureCallback(null);
5+
process.setUncaughtExceptionCaptureCallback(captureSym, common.mustNotCall());
166
require('domain'); // Should not throw.

test/parallel/test-domain-set-uncaught-exception-capture-after-load.js

Lines changed: 0 additions & 28 deletions
This file was deleted.

test/parallel/test-process-exception-capture-errors.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
const common = require('../common');
33

44
common.expectsError(
5-
() => process.setUncaughtExceptionCaptureCallback(42),
5+
() => process.setUncaughtExceptionCaptureCallback(Symbol('foo'), 42),
66
{
77
code: 'ERR_INVALID_ARG_TYPE',
88
type: TypeError,
@@ -11,13 +11,11 @@ common.expectsError(
1111
}
1212
);
1313

14-
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
15-
1614
common.expectsError(
17-
() => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()),
15+
() => process.setUncaughtExceptionCaptureCallback('foo', () => {}),
1816
{
19-
code: 'ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
20-
type: Error,
21-
message: /setupUncaughtExceptionCapture.*called while a capture callback/
17+
code: 'ERR_INVALID_ARG_TYPE',
18+
type: TypeError,
19+
message: 'The "owner" argument must be of type Symbol. Received type string'
2220
}
2321
);

0 commit comments

Comments
 (0)