Your Package Manager Is Lying to You
Your Package Manager Is Lying to You
Thu Jun 11 2026
javascript
nodejs
npm
yarn
pnpm
bun
deno
tooling
Package managers are usually treated as interchangeable tooling: install dependencies, commit the lockfile, and move on. In that framing, the only question that seems to matter is performance.
In practice, the differences run much deeper. npm, Yarn, and pnpm are built on fundamentally different models of what node_modules should be: different assumptions about how dependencies should be represented on disk, how strictly boundaries should be enforced, and how much implicit behavior the ecosystem should tolerate.
Bun and Deno go further: they challenge the model itself. Bun treats the entire developer loop as something that should feel instantaneous. Deno folds dependency management into a broader security and web-native runtime philosophy.
This is why migrations often feel disproportionate. The lockfile might be perfect and your application code untouched, yet builds break because scripts, plugins, and tools were written against a different set of invariants about the filesystem.
The real axis of difference isn't speed or disk usage, but how each tool chooses to represent and resolve dependencies.
Every package manager is a different compromise between what physically exists on disk and what the ecosystem expects to find. Those tradeoffs are easiest to see through five competing goals.
The Five Competing Goals
Every package manager is trying to optimize the same things:
Reproducible installs
Install speed and cache reuse
Disk efficiency across multiple projects
Compatibility with the Node ecosystem as it exists today
Developer experience (integrated tooling, lower friction, safer defaults)
No tool can maximize all five at once. The practical choice is usually the one whose failure mode you’re most willing to tolerate.
npm: The Pragmatic Baseline (Compatibility Over Correctness)
npm is the default package manager for Node.js and has been since its early days. If you use Node, you have npm. This gives npm a huge advantage in terms of ecosystem compatibility and developer familiarity, but it also means npm has had to make compromises to maintain that compatibility over time.
Core Model
npm flattens the dependency tree by placing packages as high as possible in node_modules, then falls back to nested subdirectories only when version conflicts force it. This simple strategy was born from pragmatism: Node's module resolution algorithm walks up the directory tree looking for node_modules, so the flatter the structure, the faster the lookup and the fewer surprises developers encounter. The strategy has persisted because it still broadly works with how the Node ecosystem is wired.
Design Philosophy
npm's philosophy is pragmatic continuity: rather than enforce a strict model of dependency access, npm prioritizes keeping the ecosystem running as it currently runs. This means tolerating patterns that are structurally impure if those patterns are common in the wild. A new, stricter model might be more correct, but it would break existing code and tooling, so npm's design philosophy is to bend toward the ecosystem rather than ask the ecosystem to bend toward it.
Strengths
This design brings several concrete advantages. Compatibility is the biggest: almost every tool in the JavaScript ecosystem was first tested and optimized for npm's semantics, so migrating away from npm often means discovering edge cases in other tooling. Setup is minimal, which matters for teams that don't want to spend cycles learning tooling; npm just works by default. And there is real value in being bundled with Node itself: it means npm is always available, always installed, and always familiar to anyone with Node on their machine.
Weaknesses
The cost of this compatibility-first approach is structural ambiguity. Hoisting can make undeclared dependencies accidentally available, which means the actual runtime dependency graph often differs from what package.json files claim. On larger codebases, this ambiguity compounds: node_modules can become very large and performance can degrade in monorepos or CI pipelines where many projects share the same machine. Install times are generally slower than newer, store-based approaches, especially when you are running the same installs repeatedly across different projects or CI runs.
The Lie It Tells
npm's lie is a useful one: it suggests that a package's runtime behavior matches its declared dependencies. In reality, a package can often reach into the hoisted tree and use packages it never declared, simply because those packages were placed somewhere reachable. The discrepancy is usually invisible until something changes—a new dependency, a version conflict, or a different install layout on a different machine—and suddenly that undeclared access no longer works.
Example
Suppose your app declares react, but one of your dependencies...