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
- Break the cycle: extract the shared code that both worker modules need into a third, dependency-free module and import that from both.
- Inspect the printed Import chain to find the offending edge and remove or invert that one import.
- If the cycle is via a barrel (index.ts), import the specific file directly instead of the barrel to avoid re-pulling the worker.
- 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
- Keep worker modules leaf-like: they should not import other worker entries.
- Avoid barrel files (index.ts) in the worker's import path to prevent accidental cycles.
- Run a static cycle check (madge, dependency-cruiser) over worker entry graphs in CI.
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.