We ran madge across five major open-source JavaScript projects. Payload CMS has 508 circular dependencies in 675 TypeScript files. Next.js has 17 in 14,556. Twenty has zero.

Nobody who works on Payload wrote a single line with the intention of creating a cycle. Every one of those 508 paths through the import graph was the result of incremental, individually reasonable decisions. This is how circular dependencies work — and it is why they appear in virtually every large JavaScript codebase, including yours.

A circular dependency is one of the few bugs that passes every check your toolchain runs. TypeScript compiles it cleanly. The tests pass. The build succeeds. The app ships. And somewhere deep in your import graph, a developer is staring at a TypeError: X is not a constructor that disappears the moment they add a console.log.

Here are the three patterns that create them, what Node.js, webpack, Rollup, and esbuild actually do with them (they don't solve the problem — they each make a different tradeoff), and how to stop them from forming.

What a circular dependency actually is