Skip to content

🐛 Bug Report — Worker Loader: "tried to defer destruction during isolate shutdown" crash during ephemeral isolate teardown #6506

@pawaca

Description

@pawaca

Environment

  • wrangler: 4.75.0
  • miniflare: 4.20260317.0
  • @cloudflare/vitest-pool-workers: 0.13.2
  • compatibility_date: 2025-01-13
  • compatibility_flags: ["nodejs_compat"]
  • Platform: macOS (Darwin 25.1.0), Node.js v22.13.1

Describe the Bug

When running vitest tests that use worker_loaders binding to create many ephemeral isolates (40+ per test file via loader.load()), workerd intermittently crashes during isolate teardown with:

*** Fatal uncaught kj::Exception: workerd/jsg/setup.c++:235: failed: 
expected queueState == QueueState::ACTIVE [1 == 0]; 
tried to defer destruction during isolate shutdown; queueState = 1

This appears to be a race condition where a JS callback in the ephemeral isolate tries to call deferDestruction() after the isolate has already entered the DROPPING (1) state in its dropWrappers() shutdown phase.

The crash is probabilistic — roughly 1 in 5 runs in our test suite. It does not affect test correctness (all assertions pass before the crash), but it causes workerd to exit unexpectedly, sometimes preventing vitest from collecting the last few test results.

Minimal Reproduction

wrangler.test.jsonc:

{
  "name": "repro",
  "main": "src/index.ts",
  "compatibility_date": "2025-01-13",
  "compatibility_flags": ["nodejs_compat"],
  "worker_loaders": [{ "binding": "LOADER" }]
}

vitest.workers.config.ts:

import { cloudflareTest } from '@cloudflare/vitest-pool-workers'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  plugins: [cloudflareTest({ wrangler: { configPath: './wrangler.test.jsonc' } })],
  test: {
    include: ['src/repro.test.ts'],
    dangerouslyIgnoreUnhandledErrors: true,
  },
})

src/repro.test.ts:

import { env } from 'cloudflare:workers'
import { describe, expect, it } from 'vitest'

describe('Worker Loader isolate teardown crash', () => {
  for (let i = 0; i < 50; i++) {
    it(`ephemeral isolate #${i}`, async () => {
      const worker = env.LOADER.load({
        compatibilityDate: '2025-01-13',
        compatibilityFlags: ['nodejs_compat'],
        mainModule: 'sandbox.js',
        modules: {
          'sandbox.js': `
            import { WorkerEntrypoint } from 'cloudflare:workers';
            export default class extends WorkerEntrypoint {
              async evaluate() { return { value: ${i} }; }
            }`,
        },
      })
      const ep = worker.getEntrypoint()
      const result = await ep.evaluate()
      expect(result.value).toBe(i)
    })
  }
})

Run 5 times — at least 1 will crash:

for i in $(seq 5); do npx vitest run --config vitest.workers.config.ts 2>&1 | grep -E "Tests|Errors|Fatal"; done

Expected Behavior

All 50 tests pass without workerd crash. Isolate teardown should handle deferred destruction gracefully when the isolate is shutting down.

Current Workaround

dangerouslyIgnoreUnhandledErrors: true in vitest config. The crash doesn't affect test correctness, just reporting.

Suggested Fix Direction

In src/workerd/jsg/setup.c++:235, the KJ_REQUIRE assertion in deferDestruction() crashes when queueState == DROPPING. For ephemeral Worker Loader isolates, this could either:

  • Silently drop the deferred item (the isolate is being destroyed anyway)
  • Drain the deferred queue before transitioning to DROPPING state

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions