node/test/parallel/test-repl-user-error-handler.js
Anna Henningsen 5e3951e0ad
repl: add customizable error handling
Whether or not an exception should be handled when it
was thrown in the REPL's context but the REPL is already
closed is something that will depend on the details of
the specific REPL instance's use case.

Adding this option lets the REPL's creator control the
details of this behavior.

This resolves a TODO recently added in bd3cba5633.

PR-URL: https://github.com/nodejs/node/pull/62188
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
2026-03-12 16:35:31 +00:00

84 lines
2.8 KiB
JavaScript

'use strict';
const common = require('../common');
const { start } = require('node:repl');
const assert = require('node:assert');
const { PassThrough } = require('node:stream');
const { once } = require('node:events');
const test = require('node:test');
const { spawn } = require('node:child_process');
function* generateCases() {
for (const async of [false, true]) {
for (const handleErrorReturn of ['ignore', 'print', 'unhandled', 'badvalue']) {
if (handleErrorReturn === 'badvalue' && async) {
// Handled through a separate test using a child process
continue;
}
yield { async, handleErrorReturn };
}
}
}
for (const { async, handleErrorReturn } of generateCases()) {
test(`async: ${async}, handleErrorReturn: ${handleErrorReturn}`, async () => {
let err;
const options = {
input: new PassThrough(),
output: new PassThrough().setEncoding('utf8'),
handleError: common.mustCall((e) => {
err = e;
queueMicrotask(() => repl.emit('handled-error'));
return handleErrorReturn;
})
};
let uncaughtExceptionEvent;
if (handleErrorReturn === 'unhandled' && async) {
process.removeAllListeners('uncaughtException'); // Remove the test runner's handler
uncaughtExceptionEvent = once(process, 'uncaughtException');
}
const repl = start(options);
const inputString = async ?
'setImmediate(() => { throw new Error("testerror") })\n42\n' :
'throw new Error("testerror")\n42\n';
if (handleErrorReturn === 'badvalue') {
assert.throws(() => options.input.end(inputString), /ERR_INVALID_STATE/);
return;
}
options.input.end(inputString);
await once(repl, 'handled-error');
assert.strictEqual(err.message, 'testerror');
const outputString = options.output.read();
assert.match(outputString, /42/);
if (handleErrorReturn === 'print') {
assert.match(outputString, /testerror/);
} else {
assert.doesNotMatch(outputString, /testerror/);
}
if (uncaughtExceptionEvent) {
const [uncaughtErr] = await uncaughtExceptionEvent;
assert.strictEqual(uncaughtErr, err);
}
});
}
test('async: true, handleErrorReturn: badvalue', async () => {
// Can't test this the same way as the other combinations
// since this will take the process down in a way that
// cannot be caught.
const proc = spawn(process.execPath, ['-e', `
require('node:repl').start({
handleError: () => 'badvalue'
})
`], { encoding: 'utf8', stdio: 'pipe' });
proc.stdin.end('throw new Error("foo");');
let stderr = '';
proc.stderr.setEncoding('utf8').on('data', (data) => stderr += data);
const [exit] = await once(proc, 'close');
assert.strictEqual(exit, 1);
assert.match(stderr, /ERR_INVALID_STATE.+badvalue/);
});