Skip to content

Commit 88306b2

Browse files
committed
repl: emit uncaughtException
The internal default repl will from now on trigger `uncaughtException` handlers instead of ignoring them. The regular error output is suppressed in that case.
1 parent 1aaa6b7 commit 88306b2

7 files changed

+194
-19
lines changed

doc/api/errors.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -1308,8 +1308,14 @@ An invalid `options.protocol` was passed.
13081308
<a id="ERR_INVALID_REPL_EVAL_CONFIG"></a>
13091309
### ERR_INVALID_REPL_EVAL_CONFIG
13101310

1311-
Both `breakEvalOnSigint` and `eval` options were set in the REPL config, which
1312-
is not supported.
1311+
Both `breakEvalOnSigint` and `eval` options were set in the [`REPL`][] config,
1312+
which is not supported.
1313+
1314+
<a id="ERR_INVALID_REPL_INPUT"></a>
1315+
### ERR_INVALID_REPL_INPUT
1316+
1317+
The input can or may not be used in the [`REPL`][]. All prohibited inputs are
1318+
documented in the [`REPL`][]'s documentation.
13131319

13141320
<a id="ERR_INVALID_RETURN_PROPERTY"></a>
13151321
### ERR_INVALID_RETURN_PROPERTY
@@ -2293,6 +2299,7 @@ such as `process.stdout.on('data')`.
22932299
[`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror
22942300
[`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE
22952301
[`EventEmitter`]: events.html#events_class_eventemitter
2302+
[`REPL`]: repl.html
22962303
[`Writable`]: stream.html#stream_class_stream_writable
22972304
[`child_process`]: child_process.html
22982305
[`cipher.getAuthTag()`]: crypto.html#crypto_cipher_getauthtag

doc/api/repl.md

+33-1
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,47 @@ global or scoped variable, the input `fs` will be evaluated on-demand as
138138
```
139139

140140
#### Global Uncaught Exceptions
141+
<!-- YAML
142+
changes:
143+
- version: REPLACEME
144+
pr-url: https://github.com/nodejs/node/pull/20803
145+
description: The uncaughtException event is from now on triggered if the
146+
repl is used as standalone program.
147+
-->
141148

142149
The REPL uses the [`domain`][] module to catch all uncaught exceptions for that
143150
REPL session.
144151

145152
This use of the [`domain`][] module in the REPL has these side effects:
146153

147-
* Uncaught exceptions do not emit the [`'uncaughtException'`][] event.
154+
* Uncaught exceptions only emit the [`'uncaughtException'`][] event if the
155+
`repl` is used as standalone program. If the `repl` is included anywhere in
156+
another application, adding this event synchronous will throw an
157+
[`ERR_INVALID_REPL_INPUT`][] error!
148158
* Trying to use [`process.setUncaughtExceptionCaptureCallback()`][] throws
149159
an [`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`][] error.
150160

161+
As standalone program:
162+
163+
```js
164+
process.on('uncaughtException', () => console.log('Uncaught'));
165+
166+
throw new Error('foobar');
167+
// Uncaught
168+
```
169+
170+
When used in another application:
171+
172+
```js
173+
process.on('uncaughtException', () => console.log('Uncaught'));
174+
// TypeError [ERR_INVALID_REPL_INPUT]: Unhandled exception listeners can not be
175+
// used in the REPL
176+
177+
throw new Error('foobar');
178+
// Thrown:
179+
// Error: foobar
180+
```
181+
151182
#### Assignment of the `_` (underscore) variable
152183
<!-- YAML
153184
changes:
@@ -661,6 +692,7 @@ For an example of running a REPL instance over [curl(1)][], see:
661692
[`'uncaughtException'`]: process.html#process_event_uncaughtexception
662693
[`--experimental-repl-await`]: cli.html#cli_experimental_repl_await
663694
[`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`]: errors.html#errors_err_domain_cannot_set_uncaught_exception_capture
695+
[`ERR_INVALID_REPL_INPUT`]: errors.html#errors_err_invalid_repl_input
664696
[`domain`]: domain.html
665697
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
666698
[`readline.InterfaceCompleter`]: readline.html#readline_use_of_the_completer_function

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,7 @@ E('ERR_INVALID_PROTOCOL',
892892
TypeError);
893893
E('ERR_INVALID_REPL_EVAL_CONFIG',
894894
'Cannot specify both "breakEvalOnSigint" and "eval" for REPL', TypeError);
895+
E('ERR_INVALID_REPL_INPUT', '%s', TypeError);
895896
E('ERR_INVALID_RETURN_PROPERTY', (input, name, prop, value) => {
896897
return `Expected a valid ${input} to be returned for the "${prop}" from the` +
897898
` "${name}" function but got ${value}.`;

lib/repl.js

+58-13
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const {
7272
ERR_CANNOT_WATCH_SIGINT,
7373
ERR_INVALID_ARG_TYPE,
7474
ERR_INVALID_REPL_EVAL_CONFIG,
75+
ERR_INVALID_REPL_INPUT,
7576
ERR_SCRIPT_EXECUTION_INTERRUPTED
7677
} = require('internal/errors').codes;
7778
const { sendInspectorCommand } = require('internal/util/inspector');
@@ -102,6 +103,20 @@ const replMap = new WeakMap();
102103
const kBufferedCommandSymbol = Symbol('bufferedCommand');
103104
const kContextId = Symbol('contextId');
104105

106+
let tmpListeners = [];
107+
let checkUncaught = false;
108+
let currentListeners = [];
109+
110+
process.on('newListener', (event, listener) => {
111+
if (event === 'uncaughtException' &&
112+
checkUncaught &&
113+
!currentListeners.includes(listener)) {
114+
// Add listener to a temporary list of listeners which should be removed
115+
// again.
116+
tmpListeners.push(listener);
117+
}
118+
});
119+
105120
try {
106121
// Hack for require.resolve("./relative") to work properly.
107122
module.filename = path.resolve('repl');
@@ -268,7 +283,7 @@ function REPLServer(prompt,
268283
// statement rather than an object literal. So, we first try
269284
// to wrap it in parentheses, so that it will be interpreted as
270285
// an expression. Note that if the above condition changes,
271-
// lib/internal/repl/recoverable.js needs to be changed to match.
286+
// lib/internal/repl/utils.js needs to be changed to match.
272287
code = `(${code.trim()})\n`;
273288
wrappedCmd = true;
274289
}
@@ -428,7 +443,6 @@ function REPLServer(prompt,
428443
}
429444

430445
self.eval = self._domain.bind(eval_);
431-
432446
self._domain.on('error', function debugDomainError(e) {
433447
debug('domain error');
434448
let errStack = '';
@@ -465,22 +479,31 @@ function REPLServer(prompt,
465479
}
466480
}
467481

468-
if (errStack === '') {
469-
errStack = `Thrown: ${self.writer(e)}\n`;
470-
} else {
471-
const ln = errStack.endsWith('\n') ? '' : '\n';
472-
errStack = `Thrown:\n${errStack}${ln}`;
473-
}
474-
475482
if (!self.underscoreErrAssigned) {
476483
self.lastError = e;
477484
}
478485

479486
const top = replMap.get(self);
480-
top.outputStream.write(errStack);
481-
top.clearBufferedCommand();
482-
top.lines.level = [];
483-
top.displayPrompt();
487+
if (options[kStandaloneREPL] &&
488+
process.listenerCount('uncaughtException') !== 0) {
489+
process.nextTick(() => {
490+
process.emit('uncaughtException', e);
491+
top.clearBufferedCommand();
492+
top.lines.level = [];
493+
top.displayPrompt();
494+
});
495+
} else {
496+
if (errStack === '') {
497+
errStack = `Thrown: ${self.writer(e)}\n`;
498+
} else {
499+
const ln = errStack.endsWith('\n') ? '' : '\n';
500+
errStack = `Thrown:\n${errStack}${ln}`;
501+
}
502+
top.outputStream.write(errStack);
503+
top.clearBufferedCommand();
504+
top.lines.level = [];
505+
top.displayPrompt();
506+
}
484507
});
485508

486509
self.resetContext();
@@ -666,10 +689,32 @@ function REPLServer(prompt,
666689
const evalCmd = self[kBufferedCommandSymbol] + cmd + '\n';
667690

668691
debug('eval %j', evalCmd);
692+
if (!options[kStandaloneREPL]) {
693+
checkUncaught = true;
694+
currentListeners = process.listeners('uncaughtException');
695+
}
669696
self.eval(evalCmd, self.context, 'repl', finish);
670697

671698
function finish(e, ret) {
672699
debug('finish', e, ret);
700+
701+
if (tmpListeners.length !== 0) {
702+
tmpListeners.forEach((tmp) => {
703+
process.removeListener('uncaughtException', tmp);
704+
});
705+
tmpListeners = [];
706+
const err = new ERR_INVALID_REPL_INPUT(
707+
'Unhandled exception listeners can not be used in the REPL');
708+
self._domain.emit('error', err);
709+
if (ret === process) {
710+
self.clearBufferedCommand();
711+
self.displayPrompt();
712+
return;
713+
}
714+
}
715+
currentListeners = [];
716+
checkUncaught = false;
717+
673718
_memory.call(self, cmd);
674719

675720
if (e && !self[kBufferedCommandSymbol] && cmd.trim().startsWith('npm ')) {

test/parallel/test-repl-pretty-custom-stack.js

-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const fixtures = require('../common/fixtures');
55
const assert = require('assert');
66
const repl = require('repl');
77

8-
98
function run({ command, expected }) {
109
let accum = '';
1110

@@ -40,8 +39,6 @@ process.on('uncaughtException', (e) => {
4039
throw e;
4140
});
4241

43-
process.on('exit', () => (Error.prepareStackTrace = origPrepareStackTrace));
44-
4542
const tests = [
4643
{
4744
// test .load for a file that throws
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const cp = require('child_process');
5+
const child = cp.spawn(process.execPath, ['-i']);
6+
let output = '';
7+
8+
child.stdout.setEncoding('utf8');
9+
child.stdout.on('data', (data) => {
10+
output += data;
11+
});
12+
13+
child.on('exit', common.mustCall(() => {
14+
const results = output.replace(/^> /mg, '').split('\n');
15+
assert.deepStrictEqual(
16+
results,
17+
[
18+
'Thrown:',
19+
'ReferenceError: x is not defined',
20+
'short',
21+
'undefined',
22+
'Foobar',
23+
''
24+
]
25+
);
26+
}));
27+
28+
child.stdin.write('x\n');
29+
child.stdin.write(
30+
'process.on("uncaughtException", () => console.log("Foobar"));' +
31+
'console.log("short")\n');
32+
child.stdin.write('x\n');
33+
child.stdin.end();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
require('../common');
3+
const ArrayStream = require('../common/arraystream');
4+
const assert = require('assert');
5+
const repl = require('repl');
6+
7+
let count = 0;
8+
9+
function run({ command, expected }) {
10+
let accum = '';
11+
12+
const output = new ArrayStream();
13+
output.write = (data) => accum += data.replace('\r', '');
14+
15+
const r = repl.start({
16+
prompt: '',
17+
input: new ArrayStream(),
18+
output,
19+
terminal: false,
20+
useColors: false
21+
});
22+
23+
r.write(`${command}\n`);
24+
if (typeof expected === 'string') {
25+
assert.strictEqual(accum, expected);
26+
} else {
27+
assert(expected.test(accum), accum);
28+
}
29+
r.close();
30+
count++;
31+
}
32+
33+
const tests = [
34+
{
35+
command: 'x',
36+
expected: 'Thrown:\n' +
37+
'ReferenceError: x is not defined\n'
38+
},
39+
{
40+
command: 'process.on("uncaughtException", () => console.log("Foobar"));\n',
41+
expected: /^Thrown:\nTypeError \[ERR_INVALID_REPL_INPUT]: Unhandled exception/
42+
},
43+
{
44+
command: 'x;\n',
45+
expected: 'Thrown:\n' +
46+
'ReferenceError: x is not defined\n'
47+
},
48+
{
49+
command: 'process.on("uncaughtException", () => console.log("Foobar"));' +
50+
'console.log("Baz");\n',
51+
// eslint-disable-next-line node-core/no-unescaped-regexp-dot
52+
expected: /^Baz(.|\n)*ERR_INVALID_REPL_INPUT/
53+
}
54+
];
55+
56+
process.on('exit', () => {
57+
assert.strictEqual(count, tests.length);
58+
});
59+
60+
tests.forEach(run);

0 commit comments

Comments
 (0)