From a8b5192fefe54b140ae923e749104b49c5d3be49 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 21 Feb 2018 21:50:20 +0100 Subject: [PATCH] repl: make last error available as `_error` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: https://github.com/nodejs/node/pull/18919 Reviewed-By: Colin Ihrig Reviewed-By: Evan Lucas Reviewed-By: Vladimir de Turckheim Reviewed-By: Michaël Zasso Reviewed-By: Tobias Nießen Reviewed-By: Gus Caplan Reviewed-By: Ruben Bridgewater Reviewed-By: James M Snell Reviewed-By: Prince John Wesley Reviewed-By: Shingo Inoue --- doc/api/repl.md | 17 +++++++ lib/repl.js | 18 +++++++ test/parallel/test-repl-underscore.js | 68 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/doc/api/repl.md b/doc/api/repl.md index 873e7449f53065..c868106f2f946a 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -142,6 +142,12 @@ global or scoped variable, the input `fs` will be evaluated on-demand as ``` #### Assignment of the `_` (underscore) variable + The default evaluator will, by default, assign the result of the most recently evaluated expression to the special variable `_` (underscore). @@ -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. + + +```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 diff --git a/lib/repl.js b/lib/repl.js index 8201630c3a6389..4bcbd1a2db09f4 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -156,6 +156,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. @@ -388,6 +390,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 @@ -796,6 +800,7 @@ REPLServer.prototype.createContext = function() { REPLServer.prototype.resetContext = function() { this.context = this.createContext(); this.underscoreAssigned = false; + this.underscoreErrAssigned = false; this.lines = []; this.lines.level = []; @@ -811,6 +816,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); }; diff --git a/test/parallel/test-repl-underscore.js b/test/parallel/test-repl-underscore.js index 91f32223e180b9..57929244ae4374 100644 --- a/test/parallel/test-repl-underscore.js +++ b/test/parallel/test-repl-underscore.js @@ -10,6 +10,7 @@ testStrictMode(); testResetContext(); testResetContextGlobal(); testMagicMode(); +testError(); function testSloppyMode() { const r = initRepl(repl.REPL_MODE_SLOPPY); @@ -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();