No circular dependencies
Why it matters
Circular dependencies cause modules to receive undefined at initialization time because JavaScript resolves the cycle by partially evaluating one module before the other is ready. This produces runtime bugs that are invisible to the type system and nearly impossible to reproduce reliably. CWE-1120 (Excessive Complexity) applies: circular graphs make the build tool's evaluation order non-deterministic and prevent tree-shaking, silently bloating your bundle. AI-generated codebases are especially prone to cycles because utility files get written to "reach back up" into components they originally served.
Severity rationale
Critical because circular imports can produce silent `undefined` values at runtime that bypass type checking and are extremely difficult to trace back to their root cause.
Remediation
Find all cycles first, then break them by moving shared logic to a file that neither participant imports. Run:
npx madge --circular src/
# or
npx dpdm --circular src/
To break a cycle: extract the shared code into a new neutral module, or use dependency injection. Prevent future cycles by adding eslint-plugin-import with the import/no-cycle rule — it fails the lint check the moment a cycle is introduced.
Detection
-
ID:
no-circular-deps -
Severity:
critical -
What to look for: Scan import statements across source files for cycles. A circular dependency exists when Module A imports from Module B, which imports from Module C (or directly back to A), creating a loop. Look for these common patterns:
- A
utilsfile that imports from a component - A component that imports from a page, which imports from that component
- A
typesfile that imports runtime values - Barrel files (
index.ts) that re-export and create unintended cycles
Focus on
src/,app/,components/,lib/,hooks/directories. If the project has a tool likemadge,dpdm, oreslint-plugin-importwith theno-cyclerule configured, note its presence. Otherwise, trace imports manually for the 10-20 largest/most central files. - A
-
Pass criteria: Enumerate all import chains among the 10-20 most central files. No circular import chains detected in the source files examined. Barrel files do not create cycles. Report even on pass: "Examined X files, 0 circular import chains found."
-
Fail criteria: One or more circular import chains are found in source files. Do not pass when a barrel file (
index.ts) re-exports create an indirect cycle — trace through barrel re-exports. -
Skip (N/A) when: The project has only 1-3 source files with no meaningful import graph. Signal: fewer than 5 JavaScript/TypeScript source files.
-
Detail on fail: Name the specific cycle found. Example:
"Circular dependency: components/UserCard.tsx → lib/auth.ts → components/UserCard.tsx"or"Barrel file src/components/index.ts creates cycle with components/Dashboard/index.ts" -
Remediation: Circular dependencies can cause subtle runtime bugs (undefined values at initialization time), slow build tools, and make refactoring unpredictable. They're especially common in AI-generated codebases where utilities "reach back up" to components.
To find all cycles, run:
npx madge --circular src/ # or npx dpdm --circular src/To fix a cycle, identify which import is creating the loop and break it by:
- Moving the shared logic to a new file that neither circular participant imports
- Using dependency injection instead of direct imports
- Restructuring barrel files to avoid re-export cycles
Prevent future cycles by adding
eslint-plugin-importwith theimport/no-cyclerule to your ESLint config.
External references
- iso-25010:2011 · maintainability.modularity — Modularity
- iso-25010:2011 · maintainability.analysability — Analysability
- cwe · CWE-1120 — Excessive Code Complexity
Taxons
History
- 2026-04-18·v1.0.0·Initial import from code-maintainability·automated