vitejs/vite · error · Error

Circular worker imports detected. Vite does not support it.

Error message

Circular worker imports detected. Vite does not support it. Import chain: ${newBundleChain.map((id) => prettifyUrl(id, config.root)).join(' -> ')}

What it means

When Vite bundles a worker entry it tracks config.bundleChain; bundleWorkerEntry at packages/vite/src/node/plugins/worker.ts:178 appends the current input and, if that input already appears in the chain, throws with the full import cycle. Vite cannot emit a worker that transitively imports itself because the bundle has no fixed point.

Source

Thrown at packages/vite/src/node/plugins/worker.ts:178

const workerOutputCaches = new WeakMap<ResolvedConfig, WorkerOutputCache>()

async function bundleWorkerEntry(
  config: ResolvedConfig,
  id: string,
): Promise<WorkerBundle> {
  const input = cleanUrl(id)

  const workerOutput = workerOutputCaches.get(config.mainConfig || config)!
  workerOutput.removeBundleIfInvalidated(input)

  const bundleInfo = workerOutput.getWorkerBundle(input)
  if (bundleInfo) {
    return bundleInfo
  }

  const newBundleChain = [...config.bundleChain, input]
  if (config.bundleChain.includes(input)) {
    throw new Error(
      'Circular worker imports detected. Vite does not support it. ' +
        `Import chain: ${newBundleChain.map((id) => prettifyUrl(id, config.root)).join(' -> ')}`,
    )
  }

  // bundle the file as entry to support imports
  const { rolldown } = await import('rolldown')
  const { plugins, rolldownOptions, format } = config.worker
  const workerConfig = await plugins(newBundleChain)
  const workerEnvironment = new BuildEnvironment('client', workerConfig) // TODO: should this be 'worker'?
  await workerEnvironment.init()

  const chunkMetadataMap = new ChunkMetadataMap()
  const workerBuildTarget = workerEnvironment.config.build.target
  const bundle = await rolldown({
    ...rolldownOptions,
    input,
    plugins: workerEnvironment.plugins.map((p) =>

View on GitHub (pinned to 89620f09af)

Solutions

  1. Break the cycle: extract the shared code that both worker modules need into a third, dependency-free module and import that from both.
  2. Inspect the printed Import chain to find the offending edge and remove or invert that one import.
  3. If the cycle is via a barrel (index.ts), import the specific file directly instead of the barrel to avoid re-pulling the worker.
  4. Lazy-load the back-edge with a dynamic import() inside a function so it is no longer a static module-graph cycle.

Example fix

// before: worker.js -> utils.js -> worker.js  (cycle)
// worker.js
import { x } from './utils'
// utils.js
import('./worker')

// after: extract shared.js with no worker import
// worker.js -> shared.js <- utils.js
Defensive patterns

Strategy: validation

Validate before calling

// Detect a static cycle in worker imports before bundling using a simple DFS.
function hasWorkerCycle(graph: Record<string, string[]>, entry: string): boolean {
  const seen = new Set<string>()
  const dfs = (n: string, path: Set<string>): boolean => {
    if (path.has(n)) return true
    if (seen.has(n)) return false
    seen.add(n); path.add(n)
    return (graph[n] ?? []).some(c => dfs(c, path))
  }
  return dfs(entry, new Set())
}

Prevention

When it happens

Trigger: Worker module A imports worker module B which imports A; a worker entry re-exports from a barrel (index.ts) that re-exports the worker itself; new Worker(new URL('./w.js', import.meta.url)) where w.js dynamically imports a module that constructs the same worker.

Common situations: Refactoring that introduced a barrel file circularly referencing a worker; shared utility that, through imports, pulls the worker entry back in; monorepo workspace cycles where a worker and a shared package import each other.


AI-assisted analysis of vitejs/vite@89620f09af (2026-08-03). Data as JSON: /data/errors/45f4e7e7d1a45f6e.json. Report an issue: GitHub.