Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

Forcing a Single Dependency Version with pnpm overrides

Two packages pull in different versions of the same transitive dependency, causing duplicate copies and subtle bugs. This page shows how to force one version across the whole pnpm workspace with pnpm.overrides.

Exact symptoms and error messages

Exact symptoms and error messages Exact symptoms and error messages in production JavaScript package workflows. Exact symptoms and error messages Exact symptoms and error messages in production JavaScript package workflows.
Exact symptoms and error messages — the core idea of this section at a glance.
$ pnpm why zod
├─┬ pkg-a 1.0.0
│ └── zod 3.22.0
└─┬ pkg-b 2.0.0
  └── zod 3.23.8
# two copies of zod in one tree

Root cause analysis

Different ranges that do not overlap force pnpm to install two copies, which duplicates module identity and can break instanceof/shared-schema checks. When you know one version is compatible for all consumers, an override collapses the graph to a single copy — the deduplication mechanic explained in Dependency Resolution Explained and applied across a workspace via the workspace configuration.

Collapsing versions Two transitive copies collapsed to one by an override. Two copies • pkg-a → zod 3.22 • pkg-b → zod 3.23 • duplicate identity One copy • override → zod 3.23.8 • single resolution • shared identity
An override rewrites the resolved version so one shared copy remains.

Duplicate copies arise when two packages depend on ranges that do not overlap — zod@^3.22 and zod@3.23.8 pinned exactly, say — so pnpm cannot satisfy both with one version and installs two. For stateless utilities this is merely wasteful disk; for packages whose identity matters (a shared schema instance, a class checked with instanceof), two copies mean two identities and subtle, hard-to-trace bugs. An override collapses the graph to one version when you know a single version is compatible for all consumers.

The risk you accept is exactly the compatibility you are asserting. An override rewrites the resolved version regardless of what each parent requested, so if one parent genuinely needed the old version, forcing the new one can break it. That is why an override is a claim — 'this version satisfies everyone' — that you should verify with the workspace's tests rather than assume.

Resolution and configuration patch

Declare the pinned version in the root package.json:

Resolution and configuration patch Declare the pinned version in the root package.json: Resolution and configuration patch Declare the pinned version in the root package.json:
Resolution and configuration patch — the core idea of this section at a glance.
{
  "pnpm": {
    "overrides": {
      "zod": "3.23.8"
    }
  }
}

Scope it narrowly (e.g. "pkg-a>zod": "3.23.8") if only one dependency path should be forced. Re-run install to regenerate the lockfile against the single version.

CLI validation and debug commands

CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows. CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows.
CLI validation and debug commands — the core idea of this section at a glance.
# Confirm a single version now resolves
pnpm why zod
# Regenerate and verify the lockfile
pnpm install --frozen-lockfile
# Check no consumer breaks against the forced version
pnpm -r test

Prevention and CI guardrails

  • Force a single version only when you have verified compatibility across consumers.
  • Prefer a scoped override (parent>dep) over a global one where possible.
  • Remove the override once upstream ranges converge naturally.
  • Re-run the workspace test suite after forcing to catch a version-specific break.
Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows. Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows.
Prevention and CI guardrails — the core idea of this section at a glance.

Scoping an override to the narrowest path

A global override is the biggest hammer: it forces a version everywhere the package appears. Often you only need it in one subtree, and pnpm lets you scope the override to a specific dependency path so the rest of the graph keeps its originally-resolved versions.

Scoped override Force a version only under one subtree. pkg-a>zod one subtree others untouched own versions narrow claim less risk
A scoped override narrows the blast radius of the compatibility claim.
{
  "pnpm": {
    "overrides": {
      "pkg-a>zod": "3.23.8"
    }
  }
}

The scoped form pkg-a>zod forces the version only under pkg-a, leaving any other consumer of zod untouched. Preferring the narrow form reduces the blast radius of the compatibility claim you are making — you assert that pkg-a works with the forced version, not that the entire ecosystem does. Reach for the global form only when every path genuinely must move together.

Overrides versus a workspace catalog

pnpm offers two related tools that solve different problems. An override forces a resolved version, including transitive dependencies you do not declare — the right tool for collapsing a duplicated sub-dependency. A catalog centralizes the versions your own workspace packages declare for their direct dependencies, so every package references catalog: instead of a hard-coded range and they stay in lockstep.

Override vs catalog When to use an override versus a catalog. Tool Scope Use for override resolved graph transitive duplicates catalog your declared deps direct-dep consistency
Catalogs keep declared versions consistent; overrides force the resolved graph.

Use a catalog to keep direct dependencies consistent across your packages — one place to bump react for the whole workspace — and reserve overrides for the stubborn transitive duplicates a catalog cannot reach. Mixing them appropriately keeps your declared versions tidy through the catalog while overrides handle the graph-level forcing, rather than overloading overrides to do both jobs and losing the clarity of which versions you actually chose.

Frequently Asked Questions

Does an override change what my published package requires?

No — pnpm.overrides affects your install tree only, not the ranges your published package declares. Consumers resolve their own graph independently.

Override vs a catalog for shared versions?

An override forces a resolved version, even for transitive deps. A catalog (pnpm) centralizes the versions your own packages declare. Use a catalog for direct deps and an override for stubborn transitive duplicates.

How narrow should a pnpm override be?

As narrow as clears the duplication. Use the scoped form parent>dep when only one subtree is affected so other consumers keep their resolved versions, and reserve the global form for when every path must move to the pinned version together.

What's the difference between an override and a catalog?

An override forces a resolved version, including transitive dependencies. A catalog centralizes the versions your own workspace packages declare for their direct dependencies. Use a catalog for consistency and overrides for stubborn transitive duplicates.

Does forcing a version risk breaking a consumer?

Yes — an override rewrites the resolved version regardless of what each parent requested, so if a parent needed the old version it can break. Treat the override as a compatibility claim and verify it with the workspace test suite.

Related

Dependency Resolution Explained