Skip to content

Commit 96f0bec

Browse files
committed
repl: make last error available as _error
This is pretty useful when trying to inspect the last error caught by a REPL, and is made to be analogous to `_`, which contains the last successful completion value. PR-URL: #18919 Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Evan Lucas <[email protected]> Reviewed-By: Vladimir de Turckheim <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Prince John Wesley <[email protected]> Reviewed-By: Shingo Inoue <[email protected]>
1 parent 5c4f703 commit 96f0bec

File tree

3 files changed

+103
-0
lines changed

3 files changed

+103
-0
lines changed

doc/api/repl.md

+17
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ global or scoped variable, the input `fs` will be evaluated on-demand as
142142
```
143143

144144
#### Assignment of the `_` (underscore) variable
145+
<!-- YAML
146+
changes:
147+
- version: REPLACEME
148+
pr-url: https://github.com/nodejs/node/pull/18919
149+
description: Added `_error` support.
150+
-->
145151

146152
The default evaluator will, by default, assign the result of the most recently
147153
evaluated expression to the special variable `_` (underscore).
@@ -162,6 +168,17 @@ Expression assignment to _ now disabled.
162168
4
163169
```
164170

171+
Similarly, `_error` will refer to the last seen error, if there was any.
172+
Explicitly setting `_error` to a value will disable this behavior.
173+
174+
<!-- eslint-skip -->
175+
```js
176+
> throw new Error('foo');
177+
Error: foo
178+
> _error.message
179+
'foo'
180+
```
181+
165182
### Custom Evaluation Functions
166183

167184
When a new `repl.REPLServer` is created, a custom evaluation function may be

lib/repl.js

+18
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ function REPLServer(prompt,
155155
self.replMode = replMode || exports.REPL_MODE_SLOPPY;
156156
self.underscoreAssigned = false;
157157
self.last = undefined;
158+
self.underscoreErrAssigned = false;
159+
self.lastError = undefined;
158160
self.breakEvalOnSigint = !!breakEvalOnSigint;
159161
self.editorMode = false;
160162
// Context id for use with the inspector protocol.
@@ -295,6 +297,8 @@ function REPLServer(prompt,
295297
internalUtil.decorateErrorStack(e);
296298
Error.prepareStackTrace = pstrace;
297299
const isError = internalUtil.isError(e);
300+
if (!self.underscoreErrAssigned)
301+
self.lastError = e;
298302
if (e instanceof SyntaxError && e.stack) {
299303
// remove repl:line-number and stack trace
300304
e.stack = e.stack
@@ -693,6 +697,7 @@ REPLServer.prototype.createContext = function() {
693697
REPLServer.prototype.resetContext = function() {
694698
this.context = this.createContext();
695699
this.underscoreAssigned = false;
700+
this.underscoreErrAssigned = false;
696701
this.lines = [];
697702
this.lines.level = [];
698703

@@ -708,6 +713,19 @@ REPLServer.prototype.resetContext = function() {
708713
}
709714
});
710715

716+
Object.defineProperty(this.context, '_error', {
717+
configurable: true,
718+
get: () => this.lastError,
719+
set: (value) => {
720+
this.lastError = value;
721+
if (!this.underscoreErrAssigned) {
722+
this.underscoreErrAssigned = true;
723+
this.outputStream.write(
724+
'Expression assignment to _error now disabled.\n');
725+
}
726+
}
727+
});
728+
711729
// Allow REPL extensions to extend the new context
712730
this.emit('reset', this.context);
713731
};

test/parallel/test-repl-underscore.js

+68
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ testStrictMode();
1010
testResetContext();
1111
testResetContextGlobal();
1212
testMagicMode();
13+
testError();
1314

1415
function testSloppyMode() {
1516
const r = initRepl(repl.REPL_MODE_SLOPPY);
@@ -153,6 +154,73 @@ function testResetContextGlobal() {
153154
delete global.require;
154155
}
155156

157+
function testError() {
158+
const r = initRepl(repl.REPL_MODE_STRICT);
159+
160+
r.write(`_error; // initial value undefined
161+
throw new Error('foo'); // throws error
162+
_error; // shows error
163+
fs.readdirSync('/nonexistent?'); // throws error, sync
164+
_error.code; // shows error code
165+
_error.syscall; // shows error syscall
166+
setImmediate(() => { throw new Error('baz'); }); undefined;
167+
// throws error, async
168+
`);
169+
170+
setImmediate(() => {
171+
const lines = r.output.accum.trim().split('\n');
172+
const expectedLines = [
173+
'undefined',
174+
175+
// The error, both from the original throw and the `_error` echo.
176+
'Error: foo',
177+
'Error: foo',
178+
179+
// The sync error, with individual property echoes
180+
/Error: ENOENT: no such file or directory, scandir '.*nonexistent.*'/,
181+
/fs\.readdirSync/,
182+
"'ENOENT'",
183+
"'scandir'",
184+
185+
// Dummy 'undefined' from the explicit silencer + one from the comment
186+
'undefined',
187+
'undefined',
188+
189+
// The message from the original throw
190+
'Error: baz',
191+
/setImmediate/,
192+
/^ at/,
193+
/^ at/,
194+
/^ at/,
195+
/^ at/,
196+
];
197+
for (const line of lines) {
198+
const expected = expectedLines.shift();
199+
if (typeof expected === 'string')
200+
assert.strictEqual(line, expected);
201+
else
202+
assert(expected.test(line), `${line} should match ${expected}`);
203+
}
204+
assert.strictEqual(expectedLines.length, 0);
205+
206+
// Reset output, check that '_error' is the asynchronously caught error.
207+
r.output.accum = '';
208+
r.write(`_error.message // show the message
209+
_error = 0; // disable auto-assignment
210+
throw new Error('quux'); // new error
211+
_error; // should not see the new error
212+
`);
213+
214+
assertOutput(r.output, [
215+
"'baz'",
216+
'Expression assignment to _error now disabled.',
217+
'0',
218+
'Error: quux',
219+
'0'
220+
]);
221+
});
222+
}
223+
156224
function initRepl(mode, useGlobal) {
157225
const inputStream = new stream.PassThrough();
158226
const outputStream = new stream.PassThrough();

0 commit comments

Comments
 (0)