Repro: @cloudflare/vitest-pool-workers hangs at teardown when a DO blockConcurrencyWhile IIFE both emits a console.* call and throws
A DurableObject whose constructor schedules ctx.blockConcurrencyWhile(async () => { ... }), and whose IIFE both (a) emits a console.* call and (b) throws, causes @cloudflare/vitest-pool-workers to hang at isolate teardown.
The test assertion passes — the hang happens after the test resolves, during vitest's process exit.
Removing either the console.* call or the throw makes vitest exit cleanly.
The hang is vitest-only. The same Worker deployed to *.workers.dev handles the same broken DO cleanly: workerd evicts the broken instance and recreates it on the next request. Other DO instances are unaffected.
Files in this gist are flat. Reproduce as:
onstart-repro/
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── wrangler.jsonc
├── src/
│ └── index.ts # `worker.ts` in this gist
└── test/
└── onstart-throw.test.ts # `onstart-throw.test.ts` in this gist
wrangler.jsonc references ./src/index.ts. Adjust the main field if you want everything flat (e.g., "./worker.ts").
npm install
npm test
Expected: the test passes (✓) and then the process hangs, requiring Ctrl-C to exit.
Delete the console.log(...) line in src/index.ts:
ctx.blockConcurrencyWhile(async () => {
- console.log('emit anything before the throw to trigger the hang');
throw new Error('Intentional throw in blockConcurrencyWhile');
});Now npm test exits in ~300 ms with the test still passing.
vitest@4.1.4@cloudflare/vitest-pool-workers@0.16.13wrangler@4.86.0compatibility_date: "2026-03-12"- macOS Darwin 25.3.0, Node 22 LTS
A worker with the same DO class was deployed to a real *.workers.dev host. All requests to the broken instance returned a 500 with the constructor's error message in 200-500 ms each (cold-start range). 20 parallel requests all returned the same error cleanly; a sibling healthy DO returned in 21 ms warm immediately afterward. Workerd evicts and recreates the broken DO on each request — there is no permanent input-gate wedge in production.
vitest-pool-workers's isolate-shutdown path appears to await drainage of the input gate for any DO that had pending work. A DO whose constructor's blockConcurrencyWhile rejected AFTER emitting console output is left in a state the teardown can't drain. Workerd itself doesn't sit on this state in production — it just evicts.
We bisected from a real product setup (a LumenizeDO subclass whose onStart() throws) down to the minimum trigger:
| Setup | Result |
|---|---|
Plain DurableObject, blockConcurrencyWhile throws, no console call |
exits ~300 ms |
Plain DurableObject, throw + post-throw SQL, no console call |
exits ~420 ms |
Plain DurableObject, projects-based vitest config |
exits |
Plain DurableObject, several tests in file, no console call |
exits |
Plain DurableObject, + console.log(...) inside the IIFE before throw |
hangs >45 s |
Same, with console.debug(JSON.stringify(...)) instead of console.log |
hangs |
| Same trigger, without try/catch wrapping the throw | hangs |
Two necessary conditions: synchronous console output AND a throw in the same IIFE.