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.

The duplication an override fixes arises when two packages depend on ranges that no single version satisfies, so pnpm installs two copies. For a stateless utility this only wastes disk, but for a package whose identity matters — a validation library whose schemas are compared by reference, a framework whose context is keyed on the module instance — two copies mean two identities, and symptoms follow that resist debugging: a schema that fails to match one produced by the other copy, a context provider that does not match its consumer. The override collapses the copies to one version you have confirmed is compatible for all consumers.

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

After applying an override, prove it took effect on every path and that nothing broke against the forced version:

# Every path should now show the forced version
pnpm why zod
# Regenerate and verify the lockfile matches
pnpm install --frozen-lockfile
# Run the workspace suite against the single version
pnpm -r test

If pnpm why still shows two versions, the override was too narrow — widen its scope or switch from a path-scoped form to a global one. A clean pnpm why plus a passing test run confirms the forced version is both applied everywhere and compatible with every consumer that depended on it.

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.
  • Force a single version only after verifying compatibility across every consumer with the workspace test suite.
  • Prefer the narrowest override scope (parent>dep) that resolves the duplication.
  • Document why each override exists so it is removed when upstream ranges converge.
  • Re-run pnpm why after any dependency change to confirm the override still applies to every path.

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.

Scoped versus global overrides

pnpm lets an override be global — forcing a version everywhere the package appears — or scoped to a specific dependency path, and choosing the narrowest form that resolves the problem limits the compatibility claim you are making. A global "zod": "3.23.8" forces that version for every package in the graph, which is right when a shared dependency must be unified across the whole workspace; a scoped "pkg-a>zod": "3.23.8" forces it only under pkg-a, leaving other consumers on their resolved versions. The scoped form reduces blast radius: you assert only that pkg-a works with the forced version, not that the entire ecosystem does.

Override scope Global versus path-scoped overrides. Scope Forces Use when global everywhere must unify all parent>dep one path one bad path default start narrow limit blast radius
Start scoped and widen only when a dependency must be unified graph-wide.

The decision follows from why the duplication exists. If two packages genuinely need to share one instance — a stateful package whose identity matters — a global override that unifies them is the point. If only one dependency path pulled in an incompatible version and the rest of the graph is fine, a scoped override fixes that path without disturbing the others. Reaching for the global form by default is a common mistake that can mask a real incompatibility elsewhere; starting scoped and widening only when necessary keeps overrides as precise, reviewable statements about specific compatibility rather than blunt, graph-wide edicts.

Overrides, catalogs, and when each fits

pnpm offers two related tools that solve different problems, and using each for its purpose keeps a workspace's version management clear. An override forces a resolved version, including transitive dependencies you never declared — the right tool for collapsing a duplicated sub-dependency or pinning a patched transitive package. 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; it does not reach transitive dependencies.

Override vs catalog When to reach for each tool. Tool Reaches Use for catalog direct deps consistency override transitive too duplicates + pins
Catalogs manage declared direct versions; overrides force resolved transitive ones.

The clean division is to use a catalog for direct-dependency consistency — one place to bump react for the whole workspace — and reserve overrides for the stubborn transitive duplicates or security pins a catalog cannot express. Overloading overrides to also manage direct-dependency versions works but obscures which versions you actually chose versus which you forced, and it makes the override list grow into something hard to reason about. Keeping declared versions in the catalog and forced resolutions in overrides means each list answers a distinct question — what we depend on, versus what we force the graph to resolve — which is what keeps version management legible as the workspace grows.

When forcing a version is the wrong fix

An override is a powerful tool, and like any powerful tool it can paper over a problem that deserves a different solution. Forcing a single version is right when the duplication is incidental — two ranges that happen not to overlap but where one version genuinely satisfies both consumers. It is the wrong fix when the version conflict reflects a real incompatibility: if one consumer truly needs the old major and another truly needs the new one, forcing a single version will break one of them, and the override merely moves the failure from install time to runtime where it is harder to diagnose.

Override or not Whether forcing a single version is safe. Why do the ranges disagree? incidental override or widen real conflict don't force behind wait or update
Force a version for incidental duplication; not for a real incompatibility.

The way to tell them apart is to understand why the ranges disagree. If a dependency pins an unnecessarily narrow range, the better fix is to widen it or update the dependency that pins it, so the resolver deduplicates naturally without a forced override. If two packages have genuinely diverged in their requirements, the honest answer may be that they cannot share a single copy yet, and the duplication — while wasteful — is correct until one side catches up. Reaching for an override reflexively, without asking whether the conflict is incidental or real, is how a workspace accumulates pins that silently hold packages back or mask incompatibilities; asking the question first keeps overrides to the cases where forcing a version is genuinely safe.

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.

Should a pnpm override be scoped or global?

As narrow as resolves the duplication. Use a scoped parent>dep override when only one path pulled in an incompatible version, so other consumers keep their resolved versions. Reserve the global form for a shared dependency that genuinely must be unified across the whole workspace.

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 packages declare for their direct dependencies. Use a catalog for direct-dependency consistency and overrides for transitive duplicates or security pins.

When is a pnpm override the wrong solution?

When the version conflict reflects a real incompatibility — one consumer truly needs the old major and another the new one. Forcing a single version then breaks one of them and moves the failure to runtime. If a range is merely too narrow, widen it so the resolver deduplicates naturally instead.

Do overrides affect consumers of my published package?

No. pnpm.overrides apply only to your own install tree, not to consumers of your package — they resolve their own graph. To influence a consumer's resolution you must change your declared dependency ranges, not your overrides.

How do I remove an override safely later?

Delete the override entry, run pnpm install to re-solve, and check pnpm why <pkg> — if the graph now deduplicates naturally because upstream ranges converged, the override is no longer needed. If duplication returns, the packages have not yet converged and the override should stay, documented, until they do.

Related

Dependency Resolution Explained