Eliminate Circular Dependencies from Your JavaScript Project

Circular dependencies in JavaScript (also known as cyclic dependencies) occur when two or more modules reference each other.

This could be a direct reference (A -> B -> A):

Direct dependency, where A imports from B, which imports from A.

or indirect (A -> B -> C -> A):

Direct dependency, where A imports from B, which imports from C, which imports from A.

While circular dependencies may not directly result in bugs, they will almost always have unintended consequences. This could manifest as slow TypeScript type-checking or frequent dev-server “JavaScript heap out of memory” crashes, but could very well introduce run-time bugs. Node.js does support circular require/import statements between modules, but it can get messy quickly. In the Node.js docs, it says:

Careful planning is required to allow cyclic module dependencies to work correctly within an application.

In my experience, the best way to deal with circular dependencies is to avoid them altogether. Circular dependencies are usually an indication of bad code design, and they should be refactored and removed if at all possible.

Checking for Circular Dependencies

While there are quite a few Node packages that perform static analysis to look for circular dependencies, they didn’t quite do the trick. Some of the packages found a few circular dependencies, while others missed all of them completely. The best circular dependency checker that I found works at the bundling layer. This webpack circular-dependency-plugin was quite comprehensive and very simple to use. To get started, I just copied the sample code from the circular-dependency-plugin docs:

// webpack.config.js
const CircularDependencyPlugin = require("circular-dependency-plugin");

module.exports = {
  entry: "./src/index",
  plugins: [
    new CircularDependencyPlugin({
      // exclude detection of files based on a RegExp
      exclude: /a\.js|node_modules/,
      // add errors to webpack instead of warnings
      failOnError: true,
      // allow import cycles that include an asyncronous import,
      // e.g. via import(/* webpackMode: "weak" */ './file.js')
      allowAsyncCycles: false,
      // set the current working directory for displaying module paths
      cwd: process.cwd(),
    }),
  ],
};

Immediately, the plugin found all sorts of circular dependencies that had been introduced over the duration of the project. Output of the circular dependency plugin

Fixing Circular Dependencies

Fixing circular dependencies on a large project can be a significant time investment. That’s why it’s best to start with the circular dependency checker from the get-go, since you can prevent them from being introduced at all.

However, in software teams, we don’t always have the luxury (misfortune) of setting up the build configuration for a project, and often we inherit codebases. So it’s often necessary to address this after it becomes a problem.

Unfortunately, adding the circular dependency plugin and immediately seeing 500 errors feels overwhelming. But fear not! It’s not always one-to-one – sometimes fixing a single import may break the chain of imports for a large number of problems, especially when it’s in core logic or shared helpers. So those 500 errors may be solved by moving code around in just five files. (Probably not, but you can dream 😤)

In Practice

There’s not always a clean one-size-fits-all solution to address circular dependency imports. It just comes down to rolling up your proverbial sleeves and getting started with it.

Ultimately, this comes down to breaking the import chain. There are a couple of options to get rid of circular dependencies. For a longer chain, A -> B -> C -> D -> A, if one of the references is removed (for instance, the D -> A reference), the cyclic reference pattern is broken.

For simpler patterns, such as A -> B -> A, refactoring may be necessary. Perhaps the modules that live in B could be moved to A. Or, necessary code could be extracted to a C that both A and B reference. If the two modules perform similar behaviors, they could also be combined into a single module. This needs to be determined on a case-by-case basis depending on unique factors for each import, but the general behavior should stand.

Fixing a large number of circular dependencies might be a significant time commitment, but it improves the maintainability of the codebase and can reduce bugs in the future. By leaving the circular dependency plugin in the webpack pipeline, it can be run frequently, and circular dependencies will be found immediately after introducing one.

Conversation
  • chase says:

    Thanks, the plugin will save my another day!

  • Lancelot says:

    thanks for your sharing !

  • JACK says:

    How do I use the plugin? Should I add the code in my index.html file? Maybe I’m a noob but there’s less infos about the way to use the plugin.

    • Dan Kelch Dan Kelch says:

      Hi Jack,

      This plugin is designed for Webpack users – if you have a webpack.config.js file, you could follow the instructions laid out in the documentation:

      // webpack.config.js
      const CircularDependencyPlugin = require('circular-dependency-plugin')
      module.exports = {
      entry: "./src/index",
      plugins: [
      new CircularDependencyPlugin({
      // exclude detection of files based on a RegExp
      exclude: /a\.js|node_modules/,
      // include specific files based on a RegExp
      include: /dir/,
      // add errors to webpack instead of warnings
      failOnError: true,
      // allow import cycles that include an asyncronous import,
      // e.g. via import(/* webpackMode: "weak" */ './file.js')
      allowAsyncCycles: false,
      // set the current working directory for displaying module paths
      cwd: process.cwd(),
      })
      ]
      }

      Some tools (such as Next.js or create-react-app) will manage the Webpack configuration behind the scenes, but they typically allow for the user to override or customize the configuration themselves.

      If you aren’t using Webpack, then this particular plugin won’t help much, but there is likely another one to suit your needs.

  • Fatai Balogun says:

    Nice one, for those that aren’t using webpack in their project. Setting up *Eslint* is a good alternative to statically detect when circular dependencies is introduced to the codebase. eslint import/no-cycle should be enabled which is from airbnb style.

  • Robbert says:

    Instead of fixing it, you can avoid these problems and use init() functions. You can take control over the order in which the code is executed. See more at : https://stackoverflow.com/questions/38841469/how-to-fix-this-es6-module-circular-dependency/42704874#42704874

  • Alalanais says:

    Someone stole your post :/ https://habr.com/en/post/447506/

  • Comments are closed.