Skip to content

Commit

Permalink
repl: make last error available as _error
Browse files Browse the repository at this point in the history
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 <cjihrig@gmail.com>
Reviewed-By: Evan Lucas <evanlucas@me.com>
Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Prince John Wesley <princejohnwesley@gmail.com>
Reviewed-By: Shingo Inoue <leko.noor@gmail.com>
  • Loading branch information
addaleax committed Mar 5, 2018
1 parent 5c4f703 commit 96f0bec
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 0 deletions.
17 changes: 17 additions & 0 deletions doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ global or scoped variable, the input `fs` will be evaluated on-demand as
```

#### Assignment of the `_` (underscore) variable
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/18919
description: Added `_error` support.
-->

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

Similarly, `_error` will refer to the last seen error, if there was any.
Explicitly setting `_error` to a value will disable this behavior.

<!-- eslint-skip -->
```js
> throw new Error('foo');
Error: foo
> _error.message
'foo'
```

### Custom Evaluation Functions

When a new `repl.REPLServer` is created, a custom evaluation function may be
Expand Down
18 changes: 18 additions & 0 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ function REPLServer(prompt,
self.replMode = replMode || exports.REPL_MODE_SLOPPY;
self.underscoreAssigned = false;
self.last = undefined;
self.underscoreErrAssigned = false;
self.lastError = undefined;
self.breakEvalOnSigint = !!breakEvalOnSigint;
self.editorMode = false;
// Context id for use with the inspector protocol.
Expand Down Expand Up @@ -295,6 +297,8 @@ function REPLServer(prompt,
internalUtil.decorateErrorStack(e);
Error.prepareStackTrace = pstrace;
const isError = internalUtil.isError(e);
if (!self.underscoreErrAssigned)
self.lastError = e;
if (e instanceof SyntaxError && e.stack) {
// remove repl:line-number and stack trace
e.stack = e.stack
Expand Down Expand Up @@ -693,6 +697,7 @@ REPLServer.prototype.createContext = function() {
REPLServer.prototype.resetContext = function() {
this.context = this.createContext();
this.underscoreAssigned = false;
this.underscoreErrAssigned = false;
this.lines = [];
this.lines.level = [];

Expand All @@ -708,6 +713,19 @@ REPLServer.prototype.resetContext = function() {
}
});

Object.defineProperty(this.context, '_error', {
configurable: true,
get: () => this.lastError,
set: (value) => {
this.lastError = value;
if (!this.underscoreErrAssigned) {
this.underscoreErrAssigned = true;
this.outputStream.write(
'Expression assignment to _error now disabled.\n');
}
}
});

// Allow REPL extensions to extend the new context
this.emit('reset', this.context);
};
Expand Down
68 changes: 68 additions & 0 deletions test/parallel/test-repl-underscore.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ testStrictMode();
testResetContext();
testResetContextGlobal();
testMagicMode();
testError();

function testSloppyMode() {
const r = initRepl(repl.REPL_MODE_SLOPPY);
Expand Down Expand Up @@ -153,6 +154,73 @@ function testResetContextGlobal() {
delete global.require;
}

function testError() {
const r = initRepl(repl.REPL_MODE_STRICT);

r.write(`_error; // initial value undefined
throw new Error('foo'); // throws error
_error; // shows error
fs.readdirSync('/nonexistent?'); // throws error, sync
_error.code; // shows error code
_error.syscall; // shows error syscall
setImmediate(() => { throw new Error('baz'); }); undefined;
// throws error, async
`);

setImmediate(() => {
const lines = r.output.accum.trim().split('\n');
const expectedLines = [
'undefined',

// The error, both from the original throw and the `_error` echo.
'Error: foo',
'Error: foo',

// The sync error, with individual property echoes
/Error: ENOENT: no such file or directory, scandir '.*nonexistent.*'/,
/fs\.readdirSync/,
"'ENOENT'",
"'scandir'",

// Dummy 'undefined' from the explicit silencer + one from the comment
'undefined',
'undefined',

// The message from the original throw
'Error: baz',
/setImmediate/,
/^ at/,
/^ at/,
/^ at/,
/^ at/,
];
for (const line of lines) {
const expected = expectedLines.shift();
if (typeof expected === 'string')
assert.strictEqual(line, expected);
else
assert(expected.test(line), `${line} should match ${expected}`);
}
assert.strictEqual(expectedLines.length, 0);

// Reset output, check that '_error' is the asynchronously caught error.
r.output.accum = '';
r.write(`_error.message // show the message
_error = 0; // disable auto-assignment
throw new Error('quux'); // new error
_error; // should not see the new error
`);

assertOutput(r.output, [
"'baz'",
'Expression assignment to _error now disabled.',
'0',
'Error: quux',
'0'
]);
});
}

function initRepl(mode, useGlobal) {
const inputStream = new stream.PassThrough();
const outputStream = new stream.PassThrough();
Expand Down

0 comments on commit 96f0bec

Please sign in to comment.