Open any monorepo larger than four packages and grep for the React version. You will find at least two. One package is on 19.1.0, another is on 19.0.2 because nobody bumped it during the last refactor, and a third pinned 19.1.1 inside its devDependencies because someone needed a specific patch. The bundler quietly dedupes most of it. The type checker quietly does not. The tests pass on CI and fail on the developer who happens to have a colder pnpm store.
This is the problem catalogs were meant to solve, and over the last year they have moved from a pnpm-specific convenience to something close to a baseline expectation. Bun shipped catalog support. Yarn has had its own version constraint mechanism for years. Renovate and Dependabot now read the catalog. npm is still the only mainstream package manager without it, and the gap is starting to look load-bearing.
What a catalog actually is
A catalog is a single source of truth for a dependency version, defined once at the workspace root and referenced by every package that needs it. The version literal disappears from individual package.json files and is replaced by a sentinel string that the package manager resolves at install time.
In pnpm, the catalog lives in pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
catalog:
react: 19.1.0
react-dom: 19.1.0
typescript: 5.7.3
zod: 3.24.1
catalogs:
react18:
react: 18.3.1
react-dom: 18.3.1A package then references the catalog by name:
{
"name": "@acme/web",
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"typescript": "catalog:"
}
}The bare catalog: refers to the default catalog. Named catalogs are referenced as catalog:react18, which is the mechanism for keeping a legacy package on the old version while the rest of the workspace moves forward.
What ships into the lockfile is the resolved version. Catalogs are not a runtime feature. The package manager rewrites the references during install, and the resolved versions are pinned into pnpm-lock.yaml or the Bun equivalent. Removing the catalog from the workspace file later is a behavior change, not a no-op, because every package that referenced it loses its declared version.
Why the old approach quietly broke teams
Before catalogs, the canonical fix for version drift was to hoist shared dependencies into the workspace root. That works for a flat monorepo with one app and a handful of libraries. It falls apart the moment two packages want to be published as separate npm artifacts, because hoisting hides the dependency from the published package.json. The library declares no React dependency, the consumer installs it without React, and the import fails at runtime with a message about hooks being called outside a component.
The other workaround was a custom script. Most teams larger than ten engineers have written one: it walks the workspace, reads every package.json, and either rewrites versions to match the root or fails the build when they diverge. These scripts exist in private repos. They are usually 200 lines, undocumented, and the engineer who wrote them has left.
Catalogs make the script obsolete by moving the constraint into the package manager. The check is no longer a separate CI step. An install with a missing catalog entry fails outright.

The cross-manager picture in 2026
Catalogs are not a standardized feature. Each package manager that supports them ships a slightly different implementation, and the differences matter the moment a team considers switching.
| Package manager | Catalog support | Where versions live | Named catalogs |
|---|---|---|---|
| pnpm | Yes, since 9.5 | pnpm-workspace.yaml | Yes |
| Bun | Yes, since 1.2 | Root package.json under workspaces.catalog | Yes, under workspaces.catalogs |
| Yarn | Indirectly, via constraints | yarn.config.cjs | No, constraints are imperative |
| npm | No | Not applicable | Not applicable |
The pnpm and Bun implementations are intentionally close on the consumer side. A package that depends on "react": "catalog:" resolves identically in both, which means a workspace can swap managers without rewriting every package.json. The author files diverge: pnpm keeps the catalog in a separate YAML, Bun stores it inside the root package.json. For teams already running both managers in parallel (CI on pnpm, local on Bun, or the reverse) the practical move is to script the conversion between the two formats and accept that the source of truth lives wherever CI runs.
Yarn's constraints engine is older and more general. It can express rules a catalog cannot, including "this package must not depend on lodash at all" or "the version of axios in apps/web must be exactly the version in apps/api." The catalog model is a special case of that, and the trade-off is the usual one: declarative is easier to read, imperative is more expressive.
npm's absence is more interesting. The npm team has discussed workspace-wide versions in the RFC tracker for over a year. The current shape is closer to overrides than to catalogs, and overrides do not survive publish. A package published from an npm workspace with overrides ships with the unpinned version in its manifest. Catalogs solve this by resolving before publish, which is why pnpm and Bun could ship the feature without changing the registry.
What changes for the team that adopts a catalog
Three things shift the moment a catalog lands in the repo.
Renovate or Dependabot PRs collapse. Where before a React minor bump produced one PR per package, it now produces one PR that edits a single line in pnpm-workspace.yaml. Renovate added catalog awareness in mid-2025; Dependabot followed. The reviewer reads one diff instead of fifteen, and the merge is atomic. That is the largest operational win, and it is the reason most teams adopt catalogs even when they were not feeling the version drift pain.
Migrations slow down on purpose. A package that wants to test the next major of a dependency cannot just bump its local package.json. It has to either pin the version explicitly (overriding the catalog) or define a named catalog and reference it. Both options force a conversation. The catalog acts as a brake on individual packages drifting forward unannounced, which is a feature in a monorepo and an annoyance in a polyrepo.
Renaming a dependency becomes a workspace concern. The catalog name is the import key. If a package switches from date-fns to temporal-polyfill unilaterally, it cannot do so by editing only its own manifest if the dependency is in the catalog. The workspace owner has to either add a new catalog entry or accept that the package writes the version inline. This is the same trade-off as a centralized dependency policy in any large codebase, and the right answer depends on whether the workspace is one product or many.
The foot-guns nobody mentions
Catalogs interact poorly with three things, and the docs are quiet about all of them.
Peer dependencies
A library that declares a peer dependency on React still has to write a version range in its own package.json because peer ranges ship to the registry. Some package managers accept a catalog reference in a peer range and resolve it to the catalog version at publish time. Others treat it as a literal, which uploads the string "catalog:" to npm and breaks every consumer. The safe pattern is to write peer ranges explicitly, and use the catalog only for direct dependencies. The version repetition stings, but the alternative is a published package that nobody can install.
TypeScript path resolution
The TypeScript language server reads package.json to figure out which types ship with which package. The catalog sentinel is not a version, so older TS versions either ignore the dependency entirely or fall back to the workspace root. TypeScript 7 resolves catalog references correctly because the native compiler reuses the package manager's resolution. TypeScript 5.x and earlier needed an editor plugin or a small shim that materializes the resolved versions into a lockfile-derived JSON before editor startup. Teams on the older toolchain still hit this.
Published packages with internal catalog references
A package that publishes to a registry must not ship a catalog reference in its manifest. Both pnpm and Bun rewrite catalog entries to the resolved version during pnpm publish or bun publish, but a team that publishes through a custom script (a Lerna-era leftover, a homegrown release pipeline, a CI step that runs npm publish directly) has to add the resolution step themselves. The failure mode is silent: the package publishes, the install on the consumer side fails with an obscure semver error, and the regression is not caught until the package hits download numbers.

What real adoption looks like
Three patterns have settled out across teams that have run catalogs for more than a quarter.
The first is the framework-anchored catalog. Everything React-related lives in a named catalog (catalog:react). Everything build-related lives in another (catalog:build). Test dependencies in a third. Two or three named catalogs, no default. The motivation is that named catalogs become groupings the reviewer can reason about. A PR titled "bump catalog:react" carries more meaning than "bump dependencies".
The second is the default-plus-legacy pattern. A single default catalog for the canonical version, plus named catalogs for packages that have not migrated. catalog:react18 exists alongside the default catalog: that points at React 19. New packages get the default; legacy packages reference the named catalog until they are migrated. This is the dominant pattern at organizations doing incremental framework upgrades, and it tends to be the on-ramp because it requires no upfront sorting.
The third is the catalog-as-policy pattern. Every dependency goes into the catalog, including dev dependencies and the linter config. No package may declare a dependency that is not in the catalog. A workspace-level lint rule (a postinstall script or a custom Renovate config) enforces it. This is the pattern at organizations with a platform team that owns dependency policy, and it converts the catalog from a convenience into a governance tool. The cost is friction. The benefit is that the platform team can deprecate, replace, or pin any dependency by editing one file.
- Framework-anchored: 2-3 named catalogs, no default
- Default-plus-legacy: a single default catalog, named catalogs for laggards
- Catalog-as-policy: every dependency in the catalog, no exceptions
None of these is correct in the abstract. The choice maps to how the workspace is governed.
Catalogs are not overrides
The closest existing feature in npm is the overrides field. The two look similar, both centralize a version and both live at the root, but they solve different problems.
| Mechanism | Scope | Published manifest | Use case |
|---|---|---|---|
| Overrides | Transitive dependencies | Not preserved | Force a deep dep to a specific version |
| Catalogs | Direct dependencies in the workspace | Resolved to literal version | Align direct deps across packages |
Overrides are a hammer for security patches and incompatibility workarounds. They do not survive publish. A library that uses overrides to force a vulnerable transitive dependency to a fixed version protects only the workspace; the published artifact still pulls the unpatched transitive on the consumer side. Catalogs cover the inverse case: the package's own declared dependencies, which do survive publish, kept consistent across the workspace.
Teams sometimes try to use catalogs as overrides by listing the transitive dependency in the catalog and relying on hoisting. This works incidentally on pnpm because of the symlink layout, and silently fails on other managers. The two features are not interchangeable. A monorepo with serious security needs uses both.
Practical implications for new and existing workspaces
A new workspace in 2026 should start with a catalog. The cost is one extra YAML block. The payoff is a year of reduced PR churn and one fewer custom script in the build folder. The decision is not interesting.
An existing workspace with significant drift is the harder case. The migration is mechanical: write a script that reads every package.json, collects unique version strings per package name, fails loudly when a package appears with more than one version, and produces a candidate catalog. The hard part is not the script. The hard part is what to do with the divergences the script surfaces. A workspace with seventeen versions of @types/node has a problem the catalog does not solve. It surfaces it. The team has to decide which version is canonical, which packages need named catalogs because they genuinely require older versions, and which divergences were accidents.
Three months is the realistic timeline for a workspace with a hundred packages. The first two weeks are tooling. The rest is conversations about why a particular package is on React 18.2 and whether moving it to 19.1 is safe.
For teams running Bun and Node in parallel, catalogs are one of the strongest reasons to keep the manifest layer compatible. A package that declares "react": "catalog:" resolves under both, which means switching the production runtime no longer requires rewriting every package. The reverse case, an older workspace that already pinned everything, does not block the runtime switch, but it does block the operational benefits of catalogs, and most teams discover this after the migration rather than before.
Where this goes next
The interesting unanswered question is whether catalogs become a registry concept. They are a package-manager feature today, which means every manager implements its own variant and the lockfile is the only enforcement point. A catalog reference in a published manifest is undefined behavior. The pragmatic fix is the one pnpm and Bun took: resolve before publish, ship literal versions, treat the catalog as build-time sugar. The principled fix is to give npm a vocabulary for declaring that a package's React dependency follows the workspace's React version, which requires a registry change nobody has proposed.
Until that happens, catalogs are a sharp tool with a known shape: workspace-only, build-time, opt-in. They reduce churn. They surface drift. They turn dependency policy from a Slack thread into a file. That is a smaller story than most tooling shifts, and it is the one most teams will benefit from this year.
