Resolving Rollup Circular Dependency Warnings in ESM Builds
Diagnose Rollup circular dependency warnings by reading the module chain, extracting shared code to a leaf module, deferring with dynamic import, or externalizing third-party cycles.
23 Mar 2026, 19:00 UTC

Circular dependency warning during Rollup analysis
Rollup prints a warning like Circular dependency: src/a.js -> src/b.js -> src/a.js during the analysis phase. The warning means the static import graph contains a loop. In ESM, a loop is allowed by the spec, but if a module reads an export from its circular partner during top-level evaluation, that export can be uninitialized at the time of access.
The useful takeaway is to treat the warning as a signal to inspect initialization order, not as a build error to silence.
Diagnostic table
Match the warning pattern to the likely structure.
| Warning pattern | Likely cause | What to check first |
|---|---|---|
A → B → A | Tight coupling between two logic files | Top-level imports of functions or classes between A and B |
A → B → C → A | Shared utility or config pulled into the chain | Which export is imported by all three modules |
A → node_modules/lib → A | Cycle inside or involving a third-party package | Whether the library is editable and whether it expects to be external |
Ordered checks
1. Read the chain from the CLI output
Run the build in the project root where rollup.config.js lives. Example command:
npm run build
or
npx rollup -c
Rollup lists the files in order. Note the full relative paths. No special permissions are required. Risk: long chains can be truncated in the terminal; increase verbosity with --silent off.
2. Identify the shared export
Open the first two modules in the loop. Look for the specific named export that crosses the boundary. Common culprits are constants, types, configuration objects, or a small utility function that both modules need.
3. Determine ownership
Ask whether the export logically belongs to A, B, or neither. If it belongs to neither, it is a candidate for extraction.
Fixes tied to findings
Extract to a leaf module
Use when two modules share a value or type and neither owns it.
Before:
// src/user.js
import { validate } from './auth.js';
export function createUser() {}
// src/auth.js
import { createUser } from './user.js';
export function validate() {}
After:
// src/schema.js
export const UserSchema = { /* ... */ };
// src/user.js
import { UserSchema } from './schema.js';
// src/auth.js
import { UserSchema } from './schema.js';
The leaf module schema.js has no imports, breaking the loop.
Defer with dynamic import
Use when a dependency is only needed inside a specific function, not at module top level.
export async function performAction() {
const { logger } = await import('./logger.js');
logger.log('Action performed');
}
Dynamic import returns a promise, so the caller must be async. This defers resolution until after the module graph is initialized. Limitation: heavy use obscures static analysis and changes call sites to async.
Externalize third-party cycles
Use when the cycle involves node_modules you cannot edit.
// rollup.config.js
export default {
input: 'src/index.js',
external: ['problematic-library'],
};
This tells Rollup not to bundle the library. Limitation: externalizing prevents Rollup from resolving internal cycles, but does not fix runtime initialization issues if the library itself relies on circular initialization. Verify the library’s documentation for ESM usage.
Escalation criteria
- The cycle involves more than five modules and extraction creates new cycles.
- The circularity is required by framework patterns such as legacy decorators or bidirectional class references.
- Externalizing a library breaks your runtime because the library is expected to be bundled.
In those cases, consider architectural changes such as introducing an event bus or dependency injection container rather than local fixes.
Verification
Build check: run the build command again and confirm the specific Circular dependency warning for the files is absent from console output.
Runtime check: start the bundled application in a browser or Node.js environment and exercise code paths that use the previously circular modules. Look for TypeError about reading properties of undefined. Do not suppress warnings via onwarn as a fix; suppression hides the risk of uninitialized variables.
Limitation: Rollup cannot guarantee runtime safety for all circular patterns. ESM allows cycles, but top-level access to an incompletely initialized export remains a developer responsibility.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.