Repository navigation
Conversation
|
Updated 1:34 PM PT - Oct 17th, 2026
❌ @robobun, your commit 2e1070a has 3 failures in
🧪 To try this PR locally: bunx bun-pr 45551That installs a local version of the PR into your bun-45551 --bun |
|
Status: draft. The change is complete and tested. It waits for the three decisions that the Notes block of the description lists. How I reproduced the problem, on // entry.cjs or entry.mjs
Promise.resolve().then(() => process.exit(0));
throw new Error("fatal");
For #34115: CI on 2e1070a: one test fails, and only on The failure reproduces with the canary binary of
I could not run the alpine binary of this branch. That it fails for the same reason is an inference from the same lane and the same output. #44544 is open and makes this test exact: it counts only the cleanups of the contexts that a The two other red tests ( |
…before what they queued An entry-like load (the entry point, a preload, a worker entry, a test file, the original Module.runMain under a runner-called override) now has two steps. The module loader loads the graph. A task of the event loop then links and evaluates it, outside any microtask checkpoint, and reports a failure before the ticks and promise jobs that the program queued. A failure of the synchronous evaluation is an uncaught exception at the throw (from a tick in a worker). A rejection after a top-level await is reported from a promise job, before the ticks. One reporter, report_entry_failure, replaces the three in run_command.rs, web_worker.rs and test_command.rs. The one-shot per-microtask hook that drained a new process.nextTick queue, the evaluated_as_cjs flag and the module-evaluate hook that tracked the start of a worker's entry are removed.
…main serve cases JSC::loadModule takes a key, so an entry load no longer asks the host to resolve bun:main before the load. bundler_compile_prelinked.test.ts counts the host calls of the loader: 5 to 4 with the graph, 252 to 251 by name. The two cases about the default export of the entry point move from bun-main-entry-point.test.ts to process-nexttick.test.js, next to the other order cases of the entry point.
458c74a to
2e1070a
Compare
Fixes #34115
Problem
process.exit(0)and then throws exits 0 with empty stderr:Promise.resolve().then(() => process.exit(0)); throw new Error("fatal"). Node v26.12.0 prints the error and exits 1.Run::start(src/runtime/cli/run_command.rs) reads the entry promise after that checkpoint ran the exit.process.nextTick, a CommonJS entry point runs promise jobs beforeprocess.nextTickcallbacks.Fix
begin_entry_load(src/jsc/VirtualMachine.rs) loads the graph withJSC::loadModule. An event-loop task,finish_entry_load, links and evaluates it outside any checkpoint and reports a failure at once.report_entry_failurereplaces three reporters.exit-code.test.ts,process-nexttick.test.js,vm.test.tsandnode-module-module.test.jsfail on main. The vendored Node suite matches main.Background
process.nextTickcallbacks (ticks) and promise jobs.Downsides
.text: +10,432 bytes.Notes
A maintainer must decide
.mjsor.tsentry with a queued promise-job exit loses the error, as on main.SyntheticSourceProvider::createDeferred([JSC] Run a CommonJS module's generator before its importing graph links, not when its fetch completes WebKit#662)? Today only the entry-like module does. The switch fixes the last "Still lost" shape and removes theisEntrycode (Bun__VM__setEntryModuleKey,Bun__VM__isEntryModuleKey).How it works
begin_entry_loadcreates the promise that the runners wait for, as they did for the promise ofloadAndEvaluateModule.load_entry_point, the preload loop, the worker wait and the test runner keep their code.EntryLoadEvaluate). The task runs in the same turn of the event loop, before the unhandled-rejection pass.emitUncaughtException,lib/internal/event_target.js).unhandledRejection.createDeferred, so its body runs when the task links the graph.moduleLoaderFetchnames the module thatbun:mainimports.report_entry_failureruns the deferred sink flush before it prints, so output that the program wrote stays before the error.JSC::loadModuletakes a key, so JSC no longer asks the host to resolvebun:mainbefore the load.bundler_compile_prelinked.test.tscounts the host calls of the loader: resolve goes from 5 to 4 with the graph, and from 252 to 251 by name.Order changes for programs that do not throw
process.nextTick, in a worker, a later test file): ticks before promise jobs. Node does the same. This is Writable.toWeb() breaks when a preload script requires node:stream #34115.node:fs, or runs after a preload): ticks before promise jobs. Node v26.12.0 does the same. Node v26.11 and older run the promise jobs first.process.nextTickof a process, when a promise job queues it, runs after the remaining promise jobs. Node does the same. This includes anode:vmcontext withmicrotaskMode: "afterEvaluate".bun:mainreads the default export of a CommonJS entry point when its body finishes. Main ran the ticks and promise jobs of the entry point first. A CommonJS server config that a tick completes starts no server now. ES module entry points always behaved this way.process-nexttick.test.jspins it.Module.runMainoverride that calls the original and then throws: the failure of the program is reported first, then the rejection of the override.uncaughtException. Main saidunhandledRejection.Measurements
Tools:
lldbbreakpoint hit counts on debug builds,llvm-sizeon release builds.valgrind,perf,straceandbloatyare not installed here.GlobalObject::drainMicrotaskscalls,console.log("hi")VM::drainMicrotaskscalls, same programawait null.textbytes (merge base 5f5799e, before a rebase)console.log("hi"),.cjs, release, 5 interleaved rounds.mjsbun:main.Bun__VM__isEntryModuleKey. Per module fetch: 1 string compare of the referrer withbun:main.-e, stdin, preload,--import, dependency, top-level await, each with a queued promise job or tick): 33 equal to Node v26.12.0. Main: 7.Still different from Node v26.12.0
exit-code.test.tshas anit.todofor it.-ewith ES module source and an--importpreload, with a queued promise job: Bun reports the error. Node runs the promise job first.bun test: the error of a test file is printed now. The exit code is still the one that the queuedprocess.exit(0)asked for.require()of an ES module that imports the CommonJS entry point back: Node throwsERR_REQUIRE_CYCLE_MODULE. Here the cycle works, and the error that the entry point throws after it is reported (main lost it). Which exports such a cycle sees changed in 3 of 40 probed shapes.Not routed
--hotreloads after the first load keeploadAndEvaluateModuleand main's report.Module.runMain(path)call outside the runner's call of an override (node:module: report a script that Module.runMain() starts and that fails #45358 reports its failures).Vendored Node tests
test-runner-tags-experimental-warning.mjs: three cases aretodounder Bun. Their child processes throw from the top level in Bun, and the process now exits before the tick that prints the warning, as Node does for such a script.test-event-capture-rejections.js: on main the chain stops after its third step, because a tick queued in anunhandledRejectionlistener is dropped. It now runs further and reaches two steps thatnode:eventsdoes not support (a globalcaptureRejectionson an emitter whose constructor was not called, and a returned thenable). Line 125 skips those two under Bun.Found and not fixed here
.cjsfile with no CommonJS syntax is transpiled as an ES module when it is preloaded or imported.unhandledRejectionlistener in the last turn of the loop is dropped.Module.runMainoverride runs withrequire()loses the ticks and promise jobs that it queued.mock.module("bun:main")from a dependency) now givesModule "bun:main" has not been loaded. Main ran the program. The fix belongs in the fork: link and evaluate the record that the load produced.Earlier PRs
#38141, #38159, #43366 and #33088 each fix one part of this. They are redundant if this lands. #43366 alone fixes #34115 and is the small alternative.
Suites
Debug build against a debug build of main: the changed test files,
bun-main-entry-point.test.ts,hot.test.ts,watch.test.ts,process.test.js,worker_threads.test.tsand 10 more worker files, 9test/cli/testfiles, 15test/cli/runfiles, 5test/cli/inspectfiles,AsyncLocalStorage.test.ts,plugins.test.ts,macro-test.test.ts,mock-module.test.ts. No failure that main does not have. Release build: all 3,661 parallel and 85 sequential vendored Node tests, same results as main after the deviation above. The two new test files also pass withBUN_JSC_validateExceptionChecks=1and the LeakSanitizer settings of the CI runner.bun build --compilewith the default format,--bytecodeand--format=cjswas checked by hand: 8 of 8 variants report the error (main: 0). After the rebase onto 017b243: the changed test files, all 79 cases ofbundler_compile_prelinked.test.ts, and 924 runs of a--hotprocess through four reloads (up to 40 in parallel, Linux debug build) with no lost reload.hot.test.ts›does not take a promise of the program's…fails in this container in about half of the runs, with main's build (4 of 8) and with this branch (3 of 8). Nothing was run on Windows or macOS here.no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/vm/vm.test.ts, test/js/node/module/node-module-module.test.js, test/bundler/bundler_compile_prelinked.test.ts