Detect, Prevent, and Fix: Circular Dependencies In JavaScript and TypeScript
A practical guide to detect and prevent circular dependencies, along with some tips to fix circular dependencies.
Introduction
In software engineering, a circular dependency is a relation between two or more modules that either directly or indirectly depend on each other to function properly. Circular dependencies may cause problems in different scales. The following is a list of problems, from the most severe ones to the least:
- The application stops working.
- There are unexpected behavior or results due to incorrect dependency resolution.
- It is difficult to test and debug the code.
- It takes long time to build the application.
We have encountered the following issue that prevents launching a storybook:
the storybook issue: Uncaught ReferenceError: Cannot access 'WEBPACK_DEFAULT_EXPORT' before initializationThis is a plausible message, and the root cause is a circular dependency.
In this article, we are going to describe ways to detect and prevent circular dependencies, along with some tips to fix circular dependencies.
Detect Circular Dependencies
There are a number of tools that can be used to detect circular dependencies.

Madge is the most popular tool that generates a visual graph of module dependencies and finds circular dependencies. It is a simple and effective developer tool.
Install Madge
Madge can be installed globally by the following command:
% yarn global add madgeIn order to generate visual graphs, Graphviz, an open source graph visualization software, needs to be installed as well.
On MacOS, Graphviz is installed via the following command:
% brew install graphviz || port install graphvizOn Ubuntu, Graphviz is installed via the following command:
% apt-get install graphvizShow dependency graph and run detective command
We use a Vite project as an example to view the dependency graph. The React-based TypeScript project can be created by the following command:
% yarn create vite circular-dependency-project --template react-ts
% cd circular-dependency-projectAdd src/ComponentA.tsx:
export const ComponentA = () => <span>Component A</span>;Add src/ComponentB.tsx, which imports ComponentA:
import { ComponentA } from './ComponentA';
export const ComponentB = () => (
<span>
<ComponentA /> in Component B
</span>
);Add src/ComponentC.tsx, which imports ComponentB:
import { ComponentB } from './ComponentB';
export const ComponentC = () => (
<span>
<ComponentB /> in Component C
</span>
);Modify src/App.tsx to display ComponentC:
import { ComponentC } from './ComponentC';
function App() {
return <ComponentC />;
}
export default App;Execute yarn dev, and we see the composed output:

Run the following Madge command:
% madge --image graph.svg --extensions ts,tsx,js,jsx src It generates a dependency graph named graph.svg. By default, Madge only checks JavaScript files. We need to explicitly specify extensions for TypeScript and JSX files.
Here is graph.svg:

It clearly shows that main.tsx depends on App.tsx, which depends on ComponentC.tsx, which depends on ComponentB.tsx, which depends on ComponentA.tsx.
Obviously, there are no circular dependencies. It can also be verified by the following detective command:
% madge --circular --extensions ts,tsx,js,jsx src
Processed 7 files (689ms)
✔ No circular dependency found!Detect deadly circular dependencies
Modify src/ComponentA.tsx to import ComponentC:
import { ComponentC } from './ComponentC';
export const ComponentA = () => (
<span>
<ComponentC /> in Component C
</span>
);Execute madge --image graph.svg --extensions tsx src, and the generated graph shows the circular dependency in red.

It can be verified by the following detective command:
% madge --circular --extensions tsx src
Processed 6 files (692ms)
✖ Found 1 circular dependency!
1) ComponentC.tsx > ComponentB.tsx > ComponentA.tsxThis circular dependency is fatal. i.e. the application stops working — the most severe consequence.
Detect subtle circular dependencies
Not all circular dependencies are fatal. Sometime, they can exist in the code temporally harmlessly, but still a time bomb.
Put back no circular dependency code to src/ComponentA.tsx:
export const ComponentA = () => <span>Component A</span>;Create src/index.ts to re-export components:
export { ComponentA } from './ComponentA';
export { ComponentB } from './ComponentB';
export { ComponentC } from './ComponentC';Modify src/ComponentB.tsx to import from './index':
import { ComponentA } from './index';
export const ComponentB = () => (
<span>
<ComponentA /> in Component B
</span>
);Modify src/ComponentC.tsx to import from './index':
import { ComponentB } from './index';
export const ComponentC = () => (
<span>
<ComponentB /> in Component C
</span>
);Modify src/App.tsx to import from './index':
import { ComponentC } from './index';
function App() {
return <ComponentC />;
}
export default App;Execute yarn dev, and it runs as expected.
You may not notice any issues, until execute madge --image graph.svg --extensions tsx src. The generated graph shows two circular dependencies in red.

It can be verified by the following detective command:
% madge --circular --extensions tsx src
Processed 7 files (781ms)
✖ Found 2 circular dependencies!
1) index.ts > ComponentB.tsx
2) index.ts > ComponentC.tsxIt is harmless at this point. However, as index.ts grows with more exports, it may introduce some fatal dependency cycles. Debugging these kinds of issues is not trivial. The error massage could be plausible — not related to circular dependencies.
Prevent Circular Dependencies
How can we prevent circular dependencies? The most obvious choice is using the ESLint rule, import/no-cycle, which ensures that there is no resolvable path back to this module via its dependencies. This rule is comparatively computationally expensive, but it helps to detect circular dependencies.
import/no-cycle has a number of options:
maxDepth: It specifies the maximal depth to go to prevent full expansion of very deep dependency trees. By default, the lint rule detects cycles of depth 1 (self-imported) to ∞ (Infinity).ignoreExternal: It is an option to prevent the cycle detection to expand to external modules. The default value isfalse.allowUnsafeDynamicCyclicDependency: It is an option to disable reporting of errors if a cycle is detected with at least one dynamic import. The default value isfalse.
import/no-cycle requires eslint-plugin-import to be installed:
% yarn add -D eslint-plugin-import After the installation, eslint-plugin-import becomes part of devDendencies in package.json:
"devDependencies": {
"eslint-plugin-import": "^2.28.1"
}Modify .eslintrc.cjs to enable the rule for import/no-cycle.
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:import/typescript',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh', 'eslint-plugin-import'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'import/no-cycle': [
'error', // can be 'warn'
{
maxDepth: 10,
ignoreExternal: true,
},
],
},
};This example has been saved in this repository.
Execute yarn lint, circular dependencies are reported as errors:
% yarn lint
yarn run v1.22.10
warning ../package.json: No license field
$ eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0
/Users/jenniferfu/funStuff/circular-dependency-project/src/ComponentB.tsx
1:1 error Dependency cycle detected import/no-cycle
/Users/jenniferfu/funStuff/circular-dependency-project/src/ComponentC.tsx
1:1 error Dependency cycle detected import/no-cycle
/Users/jenniferfu/funStuff/circular-dependency-project/src/index.ts
2:1 error Dependency cycle detected import/no-cycle
3:1 error Dependency cycle detected import/no-cycle
✖ 4 problems (4 errors, 0 warnings)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.We can use some pre-commit hook, such as husky, to warn or prevent commit changes with lint errors. But, a more convenient way is to set up IDE to warn. Here is the ESLint extension for VSCode.

During coding, we see the warning regarding circular dependencies:

Tips To Fix Circular Dependencies
We detect and prevent circular dependencies in JavaScript and TypeScript code. Here are some tips on how to fix the circular dependencies:
- Use relative imports inside of the same module, and use imports from
index.tsfrom other modules. This helps to avoid circular dependencies inside of the same module. - Follow the import paths to find the problematic code that closes the cycle. Move the code from one module to another to avoid circular dependencies.
- Compare the files with circular dependencies, and move the common code into a separate file to be imported. The rule of thumb is writing code in small files focused on a single functionality — simpler to understand, test, and maintain, with less chance of circular dependencies.
- A quick and dirty fix is combining the files with circular dependencies into one file — a counter balance of small files focused on a single functionality.
Conclusion
We have introduced Madge to show the dependency graph and detect circular dependencies. ESLint is an effective way to prevent circular dependencies, especially combined with IDE settings.
We have also suggested some practical ways to fix circular dependencies. Hopefully, this article helps you to write efficient and reliable JavaScript and TypeScript code.
Thanks for reading!
Thanks, Gerardo Alias, for working with me on Domino products.
Want to Connect?
If you are interested, check out my directory of web development articles.In Plain English
Thank you for being a part of our community! Before you go:
- Be sure to clap and follow the writer! 👏
- You can find even more content at PlainEnglish.io 🚀
- Sign up for our free weekly newsletter. 🗞️
- Follow us on Twitter(X), LinkedIn, YouTube, and Discord.






