The npm overrides feature lets you globally enforce specific versions of dependencies across your entire project, even if different packages in your package.json declare conflicting or older versions.

Let’s see this in action. Imagine you have two dependencies:

package-a depends on lodash@^4.17.10 package-b depends on lodash@^4.17.20

Without overrides, npm would try to satisfy both. Depending on the resolution strategy, you might end up with lodash@4.17.20 (satisfying both) or, in more complex scenarios, you could encounter issues if package-a specifically requires a version that package-b’s requirement breaks.

Here’s how you’d use overrides to ensure everyone uses lodash@4.17.21 regardless of what package-a and package-b say:

// package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "dependencies": {
    "package-a": "^1.0.0",
    "package-b": "^2.0.0"
  },
  "overrides": {
    "lodash": "^4.17.21"
  }
}

After running npm install, if you inspect your node_modules directory or use npm list lodash, you’ll see lodash resolved to 4.17.21, even if package-a or package-b’s own package.json files specify a different version. This is because overrides acts as a final arbiter in the dependency resolution process.

The core problem overrides solves is dependency hell, specifically when multiple dependencies require different, incompatible versions of a shared sub-dependency. This often manifests as subtle bugs, runtime errors, or even build failures when different parts of your application interact with incompatible versions of the same library. overrides provides a declarative way to break these cycles and enforce a single, known-good version.

Internally, when npm install runs, it first resolves all direct and indirect dependencies based on your package.json and the available package versions. Before finalizing the node_modules tree, it checks the overrides section. If a package listed in overrides is found in the dependency graph, npm replaces its resolved version with the version specified in overrides. This ensures that regardless of what the individual packages request, the version you specify in overrides is the one that gets installed and used.

You can override specific versions of packages, or entire groups of packages. For instance, if you wanted to force a specific version of a scoped package like @babel/core, you’d write:

{
  "overrides": {
    "@babel/core": "7.18.0"
  }
}

Or if you wanted to force all packages within a scope to a particular major version:

{
  "overrides": {
    "@types/*": "^12.0.0"
  }
}

This can be incredibly powerful for managing security vulnerabilities or ensuring compatibility with newer versions of critical libraries without having to update every single direct dependency.

One of the most nuanced aspects of overrides is how it interacts with workspace configurations and npm link. When using npm link, the linked package’s dependencies are resolved from the global node_modules or the linked package’s own node_modules, which can bypass your project’s overrides. To ensure your overrides are respected even when linking, you might need to ensure the linked package itself has compatible dependency versions or consider using npm install --force (though this is generally discouraged for production). Furthermore, overrides only affects dependencies within your project’s node_modules. If a linked package has a dependency that your project also overrides, the linked package will use its own resolved version unless explicitly managed.

The next hurdle you’ll likely encounter is understanding how npm dedupe interacts with overrides, especially in complex dependency trees.

Want structured learning?

Take the full Npm course →