Skip to content

process: run beforeExit, exit and error listeners with the event loop entered - #43741

Open
robobun wants to merge 5 commits into
mainfrom
robobun/21e7aa82/process-listeners-enter-event-loop
Open

robobun wants to merge 5 commits into
mainfrom
robobun/21e7aa82/process-listeners-enter-event-loop

Conversation

@robobun

@robobun robobun commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In a 'beforeExit', 'exit' or 'unhandledRejection' listener, socket.destroy() runs the queued nextTicks and promise jobs before it returns: then(f); socket.destroy(); g() runs f before g, unlike Node. An 'uncaughtException' listener for an entry module throw and a Bun.ModuleGraph onError do the same.
  • Cause: native code calls these listeners between loop turns, with entered_event_loop_count at 0 (dispatch_on_exit, dispatch_on_before_exit, uncaught_exception, unhandled_rejection_owned in src/jsc/VirtualMachine.rs). The enter()/exit() pair around the native close callback is the outermost one, so EventLoop::exit() drains.

Fix

  • The four dispatchers hold enter_event_loop_scope_without_checkpoint() across the listeners and their drains, so a nested pair does not drain. tick(), timers and sockets already call JS with the count above 0.
  • Under bun test the error paths take no entry: the runner can start the next test inside the report.
  • What runs after the listeners does not change. After 'beforeExit' that is the checkpoint of process.nextTick: run the tick pass only when a tick is scheduled #43826, which this PR does not touch.
  • Verified: 8 new cases in test/js/node/process/process.test.js, all failing with src/ at main. Self-reviewed: 5 concerns raised, 3 addressed, 2 rejected (Notes).

Background

  • entered_event_loop_count (src/jsc/event_loop.rs) is the depth of native entries into JS. Only the outermost EventLoop::exit() runs the microtask checkpoint (nextTicks, promise jobs), so each callback runs to completion.
  • enter_event_loop_scope_without_checkpoint() (Bun.serve, node:http: run request handlers with the event loop entered #39826) is an enter() whose exit only balances the count, for a dispatcher that drains by itself.

Downsides

  • Code that relied on jobs running inside such a call sees them later.
  • After 'exit', and after an 'unhandledRejection' that ends the loop, bun runs no jobs, so those never run. Node runs the promise jobs (Notes).
Notes

Report. Found by a differential run against node v26.3.0: 24 entry points x 39 synchronous calls, each checked for a queued promise job or nextTick that ran before the call returned. 15 cells differ, all in 'beforeExit', 'exit' and 'unhandledRejection' listeners. That run reports the same output on bun 1.4.2, 1.4.0 and 1.3.14. There is no user report.

Repro. bun d.mjs <timer|beforeExit|exit|unhandledRejection>:

import net from "node:net";
const where = process.argv[2] || "beforeExit";
const server = net.createServer(s => { s.unref(); s.on("error", () => {}); }).listen(0, "127.0.0.1", () => {
  const c = net.connect(server.address().port, "127.0.0.1", () => {
    server.unref(); c.unref(); c.on("error", () => {});
    const probe = () => {
      const order = [];
      Promise.resolve().then(() => order.push("MICROTASK"));
      process.nextTick(() => order.push("NEXTTICK"));
      order.push("before destroy()");
      c.destroy();
      order.push("after destroy()");
      console.log(where.padEnd(19), order.join(" -> "));
    };
    if (where === "timer") setTimeout(probe, 5);
    else if (where === "unhandledRejection") { process.on("unhandledRejection", probe); setTimeout(() => Promise.reject(new Error("x")), 5); }
    else process.once(where, probe);
  });
});
  • node v26.3.0, all four: before destroy() -> after destroy().
  • bun 1.4.3: timer is correct. beforeExit and unhandledRejection print before destroy() -> NEXTTICK -> MICROTASK -> after destroy(). exit prints before destroy() -> MICROTASK -> after destroy() (process.nextTick is a no-op once process._exiting is set).
  • This PR: all four match node.
  • Other native calls with the same effect in these listeners: net resetAndDestroy(), destroy() and resetAndDestroy() on the server side, and fs.watch().close(). That run also lists TLS destroy(), which I did not run. end(), write(), destroySoon() and server.close() never did it.

Where the count is 0.

  • 'beforeExit': VirtualMachine::on_before_exit runs after the loop went idle. The nextTick drain that Process__dispatchOnBeforeExit does after the emit ran at 0 too, so a nextTick or promise job that a listener queued had the same problem. The scope covers that drain.
  • 'exit': VirtualMachine::on_exit, after the loop ended, and the worker exit path ('beforeExit' and 'exit' in a worker_threads Worker had the defect too). With process.exit() the listeners run inside the JS caller's frame and have that caller's count. That was already correct from a timer. From one of the other listeners it is correct now.
  • 'unhandledRejection': a rejection that the last callback of the loop made is reported by the handle_rejected_promises() call in run_command.rs, after the loop ended. This is the repro. The call at the end of tick() has the count at 1, so a rejection that is reported while the loop is still alive was not affected (checked with a second, pending timer). The explicit drains of the --unhandled-rejections modes are inside the scope as well.
  • 'uncaughtException': uncaught_exception for a rejected entry module (run_command.rs), and a throw in a 'beforeExit' listener. From a timer or I/O callback the count was already 1.
  • Bun.ModuleGraph onError: both error paths try it first. For an unhandled rejection that is reported after the loop ended it ran at 0.

Variants checked against node, bun 1.4.3 and this PR (net destroy() and fs.watch().close() as the nested call): timer, top level, 'beforeExit', a second 'beforeExit' listener, a nextTick and a promise job queued by 'beforeExit', 'exit' on a natural exit, 'exit' after process.exit() from a timer, a nextTick, a promise job, 'beforeExit' and 'unhandledRejection', 'unhandledRejection' for a rejection from a timer, an immediate, a nextTick, an fs callback and the top level, 'uncaughtException' from a timer, from 'beforeExit', from a top-level throw before and after an await, 'uncaughtExceptionMonitor', 'rejectionHandled', 'warning', and onError for a rejection and an exception. With this PR every variant prints before -> after.

What runs after the listeners return is unchanged. I compared the three builds with and without the destroy() call:

A native call that runs the event loop from inside a listener. Bun.build() with an async plugin setup waits for that promise before it returns. Inside it, a timer, a promise job, an immediate and a nextTick run in the same order on 1.4.3 and with this PR, from a timer callback, 'beforeExit', 'unhandledRejection' and 'uncaughtException'. The wait calls tick(), which drains unconditionally, and a timer callback already had the count at 1.

bun test. uncaught_exception and unhandled_rejection_owned hand the error to the test runner, and on_unhandled_rejection can call BunTest::run, which starts the next test inside that call. Fake timer controls and matcher waits give a checkpoint per callback when the count is 0 (#43356 has the details). A Bun.ModuleGraph onError that throws also re-enters uncaught_exception while unhandled_rejection_owned is on the stack. So both paths take no entry under bun test (enter_error_handler_scope), and bun test behaves as before. The onError of a graph is not covered there.

Self-review. Concerns raised: 5. Addressed: the 'beforeExit' checkpoint without a nextTick queue (now through #43826, see the rebase paragraph), the entry that was still held under bun test on a nested report, and the length of three comments. Rejected, with the reason:

  • A drain after the default-mode 'unhandledRejection' listener. It is correct, but alone it makes test-event-capture-rejections.js fail (details below). Run a microtask checkpoint after each batch of unhandledRejection listeners, and follow Node's captureRejections #42031 has it with the node:events changes that the test needs.
  • A checkpoint after the 'exit' listeners. It must run only after a natural exit. dispatch_on_exit also emits 'exit' after a fatal error, where promise jobs from before the error are still queued and Node does not run them. It needs its own change and a differential run against Node.

Review follow-up.

  • 'beforeExit' without a nextTick queue. On 1.4.3 the checkpoint after the listeners drained only the nextTick queue, which exists only after something reads process.nextTick. process.once("beforeExit", () => Promise.resolve().then(f)) then never ran f, and Node runs it. With the entry held, a job that a listener queued before fs.watch().close() was lost in the same way. process.nextTick: run the tick pass only when a tick is scheduled #43826 fixed that checkpoint (next paragraph).
  • The drain after a default-mode 'unhandledRejection' listener is not part of this PR. With it, test/js/node/test/parallel/test-event-capture-rejections.js continues past stage 3 (it chains its stages with process.nextTick from inside the listeners) into captureRejections gaps of node:events, and fails. Run a microtask checkpoint after each batch of unhandledRejection listeners, and follow Node's captureRejections #42031 has that drain together with the node:events changes. For this listener the new test checks only the order inside destroy().
  • Not covered: a job that runs in the drain after a throwing setImmediate callback still runs with the count at 0 (maybe_drain_microtasks() after the fold), so fs.watch().close() inside it drains again. That is a different entry point. 1.4.3 and this PR behave the same there.

Rebase onto #43826. #43826 merged while this PR was open. It ends 'beforeExit' with the full checkpoint (globalObject->drainMicrotasks()), with or without a tick queue. This PR had its own change in Process__dispatchOnBeforeExit for the same gap. The rebase dropped it, and BunProcess.cpp is now identical to main. Checked on debug builds at 6d67b14b85: with src/ at main all new cases fail, and with this PR they pass. A 'beforeExit' listener's promise job runs after the listener, with and without fs.watch().close(), in a program that never reads process.nextTick, as in Node.

Second review. Two optional test points. The new cases now pipe stderr and expect it to be empty. An eighth case runs with --unhandled-rejections=none: a nextTick that the 'unhandledRejection' listener queued runs in the explicit drain of that mode, which is inside the held entry. Not added: a row for each other mode (the same guard, and warn and strict print a warning) and worker cases (a worker reaches the same two dispatch functions, and this PR changes no worker code).

Suites run on the debug ASAN build. test/js/node/process/process.test.js (179 pass), process-nexttick.test.js, process-on.test.ts, test/js/node/net/node-net.test.ts, node-net-server.test.ts, test/js/bun/net/socket.test.ts, test/js/node/watch/fs.watch.test.ts, test/js/node/worker_threads/worker_threads.test.ts, test/js/web/workers/worker.test.ts, worker-terminate-lifetime.test.ts, test/js/bun/module-graph/module-graph.test.ts, module-graph-isolation.test.ts, module-graph-io.test.ts, test/js/node/events/event-emitter.test.ts, test/js/node/async_hooks/AsyncLocalStorage.test.ts, test/js/bun/cron/in-process-cron.test.ts, test/js/bun/util/filesink.test.ts, test/js/bun/repl/repl.test.ts, test/js/node/cluster.test.ts, child_process_ipc.test.js, spawn.ipc.test.ts, node-tls-connect.test.ts, console-write.test.ts, test/js/bun/test/bun_test.test.ts, test-test.test.ts, dots.test.ts, stack.test.ts, test/cli/test/bun-test.test.ts, isolation.test.ts, test/cli/run/require-cache.test.ts. Also 51 node tests from test/js/node/test/parallel (test-process-exit*, test-process-beforeexit*, test-promise-unhandled-*, test-process-uncaught*, test-event-capture-rejections, test-worker-*exit*, test-next-tick*, test-net-*destroy*): all exit 0. After the review follow-up: the 17 node tests that mention beforeExit (all exit 0), spawn-stdin-readable-stream.test.ts, and test/js/node/process/stdin/stdin-fixtures.test.ts. After the rebase: process.test.js (180 pass), process-nexttick.test.js, fs.watch.test.ts, node-net-server.test.ts, filesink.test.ts, repl.test.ts, module-graph.test.ts, bun_test.test.ts, bun-test.test.ts, worker_threads.test.ts, the 51 node tests and the 17 node tests that mention beforeExit: no failure except process (no USER variable).

Failures, all the same with src/ at main or on bun 1.4.3:

  • process.test.js process: the container has no USER variable. It passes with USER set.
  • node-net.test.ts (10) and socket.test.ts (1): no DNS or outside network in the container.
  • worker-terminate-lifetime.test.ts terminate() while dns.lookup() is in flight: LeakSanitizer reports 16 bytes from node_fs_binding::Binding::new.
  • AsyncLocalStorage.test.ts re-entering a storage inside run() and 7 leak tests of test/cli/run/require-cache.test.ts: timeouts of the debug build.
  • worker.test.ts and module-graph-isolation.test.ts: one 5 s timeout each in the full run. Both pass alone.
  • stdin-fixtures.test.ts (3): the test kills the child after 1 s, and the debug build needs 1.2 s for the fixture. The fixtures print the same output as on 1.4.3 when I run them by hand.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/process/process.test.js

@robobun

robobun commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:02 PM PT - Sep 23rd, 2026

❌ @robobun, your commit 940ecf4 has 1 failures in Build #120164 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 43741

That installs a local version of the PR into your bun-43741 executable, so you can run:

bun-43741 --bun

@robobun

robobun commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on bun 1.4.3 (Linux x64) with the script in the Notes of the PR body. In a 'beforeExit', 'exit' or 'unhandledRejection' listener, socket.destroy() runs the queued nextTick and promise job before it returns. node v26.3.0 runs them after the listener returns.
  • The 8 new cases in test/js/node/process/process.test.js fail on bun 1.4.3 and on a debug build of main (6d67b14b85). They pass with this PR (process: run beforeExit, exit and error listeners with the event loop entered #43741).
  • CI on 940ecf49 (build 120164): 180 of 181 jobs pass. The one red test is test/js/bun/spawn/spawn.test.ts (an idle reader stopped at the highwater mark does not keep the process alive) on debian 13 x64-asan. The child's stderr has a LeakSanitizer line, ptrace appears to be blocked ... Child exited with signal 42. The same test is red with the same output in the builds of unrelated PRs (120167, 120162, 120161, 120160, 120109), so this PR does not cause it. It is reported for main-break triage.

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 3e8e6f72-4808-4e80-9b22-b1272cb33122

📥 Commits

Reviewing files that changed from the base of the PR and between 3b92f75 and 12be3dc.

📒 Files selected for processing (2)
  • src/jsc/VirtualMachine.rs
  • test/js/node/process/process.test.js

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


Walkthrough

Exit and beforeExit dispatch now enter no-checkpoint event-loop scopes. Uncaught-exception and unhandled-rejection handling use a shared scope outside bun:test. New subprocess tests check Promise job and nextTick timing across callback contexts.

Changes

Event-loop dispatch behavior

Layer / File(s) Summary
Dispatch scope and handler behavior
src/jsc/VirtualMachine.rs
Exit and beforeExit dispatch enter no-checkpoint scopes. Uncaught-exception and unhandled-rejection handling use a shared scope unless bun:test is active.
Callback timing regression coverage
test/js/node/process/process.test.js
Subprocess tests check queued Promise jobs and nextTick callbacks during socket destruction across six callback contexts. A separate beforeExit test checks Promise job timing after fs.watch().close().

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 12be3

No actionable issue is established for the process callback timing changes; the PR is mergeable after normal checks.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: dispatching process exit and error listeners with the event loop entered.
Description check ✅ Passed The description provides detailed problem, cause, fix, scope, drawbacks, test coverage, and verification results. It uses different headings from the template, but it includes the required information…

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/bindings/BunProcess.cpp Resolved
Comment thread src/jsc/event_loop.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the follow-up commits (2124b13, 3b92f75) and found no new bugs; since this changes when the event loop counts itself as entered for four process-level dispatchers and the description lists deliberate behavior tradeoffs under "Downsides", a human look is still worthwhile.

What was reviewed:

  • enter_error_handler_scope returning None under bun test: both error paths now hold no guard at all before handing off to the runner, so the nested-guard case from the previous round no longer applies.
  • Process__dispatchOnBeforeExit fallback: vm.drainMicrotasks() matches what JSNextTickQueue::drain does on an empty queue, and the moved RETURN_IF_EXCEPTION still covers both branches. Checked a promise job that reads process.nextTick for the first time inside that fallback — its tick is not drained afterwards, but the same program ran neither the job nor the tick on the base, so this is not a regression.
  • Guard balance in uncaught_exception: the paths that skip the guard's drop are the main-thread process_exit calls that never return; the worker paths all fall through to a normal return.
Extended reasoning...

The change wraps Process__dispatchOnExit, Process__dispatchOnBeforeExit, uncaught_exception and unhandled_rejection_owned in src/jsc/VirtualMachine.rs with an EventLoopEnterNoCheckpointGuard (skipped under bun test for the two error paths), adds a vm.drainMicrotasks() fallback in src/jsc/bindings/BunProcess.cpp when no nextTick queue exists, and adds seven subprocess tests to test/js/node/process/process.test.js. It touches no security-sensitive surface. Two of the four findings from the previous run were addressed by the follow-up commits; the Mode::Bun arm in unhandled_rejection_owned still returns without a drain, which the PR description frames as an accepted downside tracked elsewhere, and that open tradeoff plus the event-loop-semantics scope is why this is a defer rather than an approve.

@robobun

robobun commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

A companion change is ready on robobun/57a8d138/drain-after-throw-entered (commit c2870a7). It is not open as its own PR. It fixes the statement that follows the one this PR fixes, and only the two together match Node.

What it covers. In report_error_or_terminate (src/jsc/Task.rs), this PR holds the count while uncaught_exception() calls the listener. The next statement, maybe_drain_microtasks(), still runs the nextTicks and promise jobs of the callback that threw with entered_event_loop_count at 0. The same helper ends a setImmediate batch whose last callback threw. The branch holds the count for that drain, as exit() does for its own checkpoint (src/jsc/event_loop.rs, +13/-5), and adds 13 cases to test/js/node/timers/node-timers.test.ts.

Measured. Debug builds on main 6d504dd, with fs.watch().close() as the nested call. "wrong" is before -> NEXTTICK -> MICROTASK -> after.

the call is made from node v26.3.0 main this PR the branch both
a job after a setImmediate callback threw ok wrong wrong ok ok
a job after an http.Server 'connection' listener threw ok wrong wrong ok ok
the 'uncaughtException' listener for that 'connection' throw ok wrong ok wrong ok

The source files do not overlap, and the commits of this PR apply on the branch without conflict. On the combined build, node-timers.test.ts (37 tests) and the socket.destroy() returns before ... block of this PR (7 cases) pass. The 13 new cases fail on main and with this PR alone.

Open point: bun test. This PR takes no scope under bun test. The branch holds under bun test too:

The two statements should follow one rule. Maintainers: I can push the commit to this PR, or open it as a PR stacked on this branch. Say which you prefer.

@alii

alii commented Sep 24, 2026

Copy link
Copy Markdown
Member

@robobun #43826 merged, and it also changes the beforeExit checkpoint (beforeExit now ends with the same checkpoint that runs the tick pass and the microtasks). Please rebase this onto main. BunProcess.cpp and process.test.js changed there, so expect conflicts.

Then check whether the Process__dispatchOnBeforeExit change here is still needed, and drop it if #43826 already covers it. Keep the rest: the four dispatchers that hold the event loop entered, and the 7 new cases.

…ion listeners with the event loop entered

Native code emits these process events between event loop turns, with
entered_event_loop_count at 0. A native call made by a listener that
dispatches a callback through enter()/exit() (socket.destroy() running the
close callback, fs.watch().close()) was then the outermost pair, so
EventLoop::exit() ran the nextTick queue and the promise jobs inside that
call, before it returned. The onError of a Bun.ModuleGraph is reached
through the same two error paths.

Hold enter_event_loop_scope_without_checkpoint() in the four dispatchers,
as the Bun.serve request paths do. What runs after the listeners return
does not change. Under bun test the error paths release it before they
call the runner, which can start the next test from there.
uncaught_exception and unhandled_rejection_owned take no entry under bun
test. A Bun.ModuleGraph onError that throws re-enters uncaught_exception
with the outer entry still held, and the runner could then start the next
test with the count at 1.

The tests now also check what runs once the listener has returned, and
cover a 'beforeExit' listener in a program without a nextTick queue.
@robobun
robobun force-pushed the robobun/21e7aa82/process-listeners-enter-event-loop branch from 3b92f75 to 12be3dc Compare September 24, 2026 02:11
@robobun

robobun commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (6d67b14b85, with #43826).

  • BunProcess.cpp conflicted. I took the version of main. process.test.js merged without a conflict.
  • The Process__dispatchOnBeforeExit change is not needed any more, so I dropped it. process.nextTick: run the tick pass only when a tick is scheduled #43826 ends 'beforeExit' with globalObject->drainMicrotasks(), with or without a tick queue, and that covers the gap my change was for. Checked on a debug build: a 'beforeExit' listener's promise job runs after the listener, with and without fs.watch().close(), in a program that never reads process.nextTick.
  • Kept: the four dispatchers and the 7 new cases. On debug builds at that commit, all 7 fail with src/ at main and pass with this PR. I renamed the seventh case to fs.watch().close() in a 'beforeExit' listener returns before the queued promise job runs, because the checkpoint that its old name described is now the one from process.nextTick: run the tick pass only when a tick is scheduled #43826.
  • The diff is now src/jsc/VirtualMachine.rs (+12 lines) and the tests. The PR body describes this state.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline test nits, I re-checked this push's new enter_error_handler_scope (src/jsc/VirtualMachine.rs:2087): under bun test neither uncaught_exception nor unhandled_rejection_owned takes an entry now, so the ModuleGraph onError-throws re-entry into uncaught_exception also reaches BunTest::run with the count unchanged. I also checked the guard held across the noreturn process_exit calls in uncaught_exception and across the Strict/Warn re-entry from unhandled_rejection_owned — nested guards only balance the count, and the beforeExit drain the earlier version relied on is now unconditional in the base (BunProcess.cpp:866, #43826).

Extended reasoning...

The Rust change is 12 lines: RAII no-checkpoint guards around the exit/beforeExit dispatchers and a bun-test-gated guard at the top of the two error dispatchers in src/jsc/VirtualMachine.rs, plus seven subprocess tests. No security-sensitive surface. The two earlier substantive concerns about post-listener drains on the 'exit' and default-mode 'unhandledRejection' paths are unchanged in this push and were rejected by the author as accepted downsides, so they are not restated; a maintainer is already engaged on the thread.

Comment thread test/js/node/process/process.test.js Resolved
Comment thread test/js/node/process/process.test.js Resolved
…ons mode

The new cases pipe stderr and expect it to be empty. One more case runs
with --unhandled-rejections=none: the nextTick that the listener queued
runs in the explicit drain of that mode, inside the held entry.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants