Repository: nodejs/node
Pull request: repl: handle exceptions from async context after close

a9da9ffc04c923f383 recently restructured async context handling in the REPL source so that it no longer uses the domain module, and instead performs its own async context tracking.

That change was not intended to be breaking, but it affected behavior for uncaught exceptions thrown after the REPL was closed. Those would now be treated as uncaught exceptions on the process level, which is probably not intentional in most situations.

While it's understandably not great that we handle execptions after closing the REPL instance, I am confident that it's best to restore the previous behavior for now and add more granular handling options separately and intentionally in a (potentially semver-major) follow-up change.

Refs: https://github.com/nodejs/node/pull/61227

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/repl.js
patch: @@ -195,7 +195,12 @@ function setupExceptionCapture() {
 
   process.addUncaughtExceptionCaptureCallback((err) => {
     const store = replContext.getStore();
-    if (store?.replServer && !store.replServer.closed) {
+    // TODO(addaleax): Add back a `store.replServer.closed` check here
+    // as a semver-major change.
+    // This check may need to allow for an opt-out, since the change in
+    // behavior could lead to DoS vulnerabilities (e.g. in the case of
+    // the net-based REPL described in our docs).
+    if (store?.replServer) {
       store.replServer._handleError(err);
       return true; // We handled it
     }

filename: test/parallel/test-repl-uncaught-exception-after-input-ended.js
patch: @@ -0,0 +1,23 @@
+'use strict';
+const common = require('../common');
+const { start } = require('node:repl');
+const { PassThrough } = require('node:stream');
+const assert = require('node:assert');
+
+// This test verifies that uncaught exceptions in the REPL
+// do not bring down the process, even if stdin may already
+// have been ended at that point (and the REPL closed as
+// a result of that).
+const input = new PassThrough();
+const output = new PassThrough().setEncoding('utf8');
+start({
+  input,
+  output,
+  terminal: false,
+});
+
+input.end('setImmediate(() => { throw new Error("test"); });\n');
+
+setImmediate(common.mustCall(() => {
+  assert.match(output.read(), /Uncaught Error: test/);
+}));