Skip to content

Fix: pnpm Catalog Protocol Not Working — Cannot Find Catalog, Resolution Errors, and Lockfile Issues

FixDevs ·

Part of:  JavaScript & TypeScript Errors

Quick Answer

Fix pnpm 9.5+ catalog protocol errors — Cannot find catalog default, ERR_PNPM_CATALOG_ENTRY_INVALID_SPEC, stale lockfile state, and tool incompatibility with catalog: references in monorepos.

What the Error Looks Like

I started rolling out pnpm catalogs across a twelve-package monorepo last summer and within an hour I had hit four distinct failure modes that the release notes glossed over. If you have ever managed dependency versions across a monorepo by hand, bumping React in twelve package.json files and praying you got them all, catalogs are the feature you have been waiting for. They also have failure modes nobody warned me about.

You run pnpm install after adding catalog: references and see:

ERR_PNPM_CONFIG_DEP_CATALOG_NOT_FOUND  Cannot find catalog 'default' for workspace package './packages/web'

Or after refactoring pnpm-workspace.yaml:

ERR_PNPM_CATALOG_ENTRY_INVALID_SPEC  Catalog entry 'react' has invalid spec

Or your IDE shows red squiggles even though pnpm install succeeded:

TS2307: Cannot find module 'react' or its corresponding type declarations.

These three errors look unrelated but they all come from the same gap. The catalog protocol is new (pnpm 9.5, released June 2024), and the tools that read package.json directly, TypeScript, ESLint, Renovate, Dependabot, IDE language servers, are still catching up to it. The fix is rarely catalog config itself. It is usually a tool that does not understand the catalog: prefix yet.

How pnpm Catalogs Actually Resolve

A catalog is a named map from package name to version specifier, defined in pnpm-workspace.yaml:

packages:
  - 'packages/*'

catalog:
  react: ^18.3.1
  react-dom: ^18.3.1

catalogs:
  ui:
    tailwindcss: ^3.4.10
    clsx: ^2.1.1

Workspace packages reference the catalog instead of pinning a version directly:

{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:",
    "tailwindcss": "catalog:ui",
    "clsx": "catalog:ui"
  }
}

When pnpm builds the dependency graph it sees the catalog: token and substitutes the version from the matching catalog at install time. The lockfile records the resolved version, not the catalog: reference, so reproducible installs still work. The unresolved catalog: string never reaches node_modules or runtime.

The catch is that every tool that reads package.json independently has to know about the catalog: token. Most legacy tools see "react": "catalog:" and treat the literal string catalog: as the version. That mismatch is the root cause of almost every catalog-related failure I have hunted down.

Solution 1: Use the Right catalog: Syntax

Two syntaxes that I see confused most often.

{
  "dependencies": {
    "react": "catalog:",
    "tailwindcss": "catalog:ui"
  }
}

catalog: with no name means “use the default catalog defined under the top-level catalog: key in pnpm-workspace.yaml.” catalog:ui means “use the catalog named ui defined under catalogs.ui in pnpm-workspace.yaml.”

The typo I make at least once a quarter:

"react": "catalog:default"

There is no default catalog name. The default catalog is the unnamed one. catalog:default fails with Cannot find catalog 'default'. The fix is to drop the name:

"react": "catalog:"

This is the single most common error I see when teams roll catalogs out for the first time.

Solution 2: Declare Every Cataloged Package

pnpm install enforces that every catalog: reference resolves to an entry. If a workspace package contains "foo": "catalog:" but foo is never declared under catalog: in pnpm-workspace.yaml, install fails:

# pnpm-workspace.yaml
catalog:
  react: ^18.3.1
  react-dom: ^18.3.1
  # forgot to declare zod here
{
  "dependencies": {
    "react": "catalog:",
    "zod": "catalog:"
  }
}

The fix is to add the missing entry:

catalog:
  react: ^18.3.1
  react-dom: ^18.3.1
  zod: ^3.23.8

I now keep a CI step that runs pnpm install --frozen-lockfile on every pull request specifically to catch this. The first time a teammate forgets to declare an entry, CI fails before main does.

Solution 3: Re-run pnpm install After Catalog Changes

Editing pnpm-workspace.yaml does not automatically regenerate the lockfile. I have lost time to phantom version mismatches that turned out to be stale lockfile state. After any change to a catalog entry, run:

pnpm install

If the lockfile and the catalog diverge, pnpm refuses to install in --frozen-lockfile mode (which is what CI uses). The fix is always to commit the regenerated lockfile.

For larger refactors I run:

pnpm install --no-frozen-lockfile

then review the lockfile diff before committing. A surprising lockfile diff usually means a transitive dependency upgraded as a side effect of the catalog change. Catching that in a code review beats discovering it in production.

Solution 4: Tooling Compatibility

The hardest catalog issues are not pnpm errors at all. They are downstream tools that do not understand the catalog: token. The situation as of mid-2026:

ToolStatusWorkaround
TypeScriptWorks (resolves from node_modules, not package.json)none
ESLintWorks for most rulesnone for typical config
RenovateNative catalog manager since 38.xenable pnpm-catalog manager
DependabotPartial: opens PRs but may misread cataloged versionsreview PR diffs by hand
npm audit / yarn auditRefuses to parse catalog: referencesrun pnpm audit instead
IDE language serversMostly fine via node_modulesrestart language server after install
Older bundlers and codegen toolsMay read package.json directlyupgrade or pin versions outside catalogs

When I onboard a tool that misbehaves with catalogs, my first question is whether the tool reads dependency versions from package.json directly (broken with catalogs) or from node_modules/.../package.json (always works with catalogs). The latter is correct.

How Catalogs Compare to Other Version-Pinning Approaches

Before pnpm 9.5, I had used three patterns for centralized version management across monorepos, and each had a failure mode that catalogs fix.

Manual pinning. Every package.json lists the exact version. Bumping React across twelve workspace packages means twelve diffs and twelve chances to forget one. I have shipped a monorepo to production with eleven packages on React 18.3.1 and one on 18.2.0 because I missed it in code review. The runtime symptom was hooks throwing on suspense boundaries, and the root cause took an afternoon to find.

npm’s overrides field. Lets the root package.json force a specific version of a transitive dependency. Useful for security patches. Useless for cross-workspace version unification because overrides only affects what npm resolves, not what each workspace’s package.json says. Source-of-truth confusion stays.

yarn’s resolutions field. Similar to npm overrides, with the same limitation. Yarn also has Plug’n’Play which sidesteps some node_modules issues but does not address version sprawl across workspace package.json files.

syncpack. A CLI tool that lints workspace package.json files and complains when versions drift. I used it for two years and it worked, but it is reactive: it tells you after the drift happened. Catalogs make drift impossible because the workspace files no longer pin a version at all. The catalog is the single source of truth and the workspace files just reference it.

The other piece that catalogs solve cleanly is “I want React to be at the same version as react-dom.” With manual pinning, nothing enforces the invariant. With catalogs, both reference the same catalog entries, and bumping one without the other requires editing two lines in the same file. The intent becomes visible.

Debugging Catalog Resolution

When something is not behaving the way I expect, the first command I run is:

pnpm why react

This shows the resolved version and which packages depend on it. If pnpm why react returns multiple versions, I have either a catalog miss (some package is pinning a different version directly) or a transitive dependency forcing an older copy.

pnpm list --depth=0 --json | jq '.[] | .dependencies.react'

I use this when I want to confirm that every workspace package resolved to the same version. The jq filter walks the workspace projects and prints what each one got for React.

To inspect the catalog itself without parsing pnpm-workspace.yaml by hand:

pnpm config get catalogs

This dumps the merged catalog state pnpm is using. Useful when a chained config file (root + per-package overrides) makes the effective catalog hard to read.

If a workspace package fails to resolve a catalog entry and the error message is unclear, run install in debug mode:

pnpm install --reporter=ndjson 2>&1 | grep -E "catalog|workspace"

The ndjson reporter prints structured events for catalog substitution and workspace linking. I have used this to track down a case where a typo in pnpm-workspace.yaml had the catalog under catalogs: (plural) when it should have been catalog: (singular) for the default catalog. The error message just said “not found” with no hint that the YAML key was wrong.

Migration Recipe From Manual Pinning

The migration I recommend, based on rolling this out across two monorepos:

Step 1. Audit current versions. Across all workspace package.json files, list every dependency and the version it pins. I use a one-liner:

fd package.json packages/ -x jq '.dependencies // {}' \
  | jq -s 'add | to_entries | sort_by(.key)'

This shows the union of all dependencies. Versions that differ across packages are the candidates for catalog-ization. Versions that are already aligned are still good catalog candidates because the catalog locks the alignment.

Step 2. Move shared versions into pnpm-workspace.yaml. Start with the highest-shared packages: React, TypeScript, the linter, the test runner. These are the ones where drift causes the most pain.

catalog:
  react: ^18.3.1
  react-dom: ^18.3.1
  typescript: ^5.5.3
  vitest: ^2.0.0

Step 3. Replace pinned versions with catalog: references in every workspace package’s package.json. A find-and-replace tool helps here. I use sd:

fd package.json packages/ -x sd '"react": "[^"]+"' '"react": "catalog:"'

Repeat for each cataloged package.

Step 4. Run pnpm install and commit the lockfile. The lockfile will look almost identical because the resolved versions did not change, only the way they are referenced changed.

Step 5. Add a CI guard. I add pnpm install --frozen-lockfile to every PR build specifically to catch missing catalog entries before they reach main.

The whole migration takes a few hours for a medium monorepo. The payoff is permanent: no more “did I update React in every package?” review item.

Sneaky Failures From the Wild

These are the catalog-related failures I have hit that are not in any docs I have read.

Mixing catalog and direct versions for the same package. If packages/web/package.json has "react": "catalog:" and packages/api/package.json has "react": "^18.2.0", pnpm allows it but the two packages can resolve to different React versions. That breaks React’s “single copy” invariant and surfaces as nightmare runtime errors. Use the catalog reference everywhere or nowhere, with no mixing.

Catalog entries narrower than what the lockfile already resolves to. Change react: ^18.3.0 to react: 18.2.0 after the lockfile resolved to 18.3.1, and pnpm install --frozen-lockfile fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The fix is to regenerate the lockfile.

workspace: and catalog: confused. workspace:* references a local workspace package. catalog: references a shared external version. They look similar but cannot be combined. Trying "react": "workspace:catalog:" is invalid syntax and the error message does not always make the distinction clear.

peerDependencies cataloged without devDependencies cataloged. If packages/ui/package.json lists "react": "catalog:" as a peerDependency but the root package.json has no react in devDependencies, your tests fail because there is no React in node_modules. Also catalog react as a devDependency at the workspace root, or in each package’s own devDependencies.

IDE not refreshing after catalog updates. VS Code and JetBrains IDEs cache module resolution. After changing a catalog entry and running pnpm install, the IDE may still resolve to the old version. Restart the TypeScript language server (Cmd-Shift-P > “TypeScript: Restart TS Server” in VS Code) or the whole IDE for JetBrains. I have spent more time chasing this than I want to admit.

Renovate opening one PR per workspace package instead of one consolidated PR. Older Renovate versions did not understand catalogs and would open a PR per catalog: reference. Upgrade Renovate to 38.x or later and enable the catalog manager in renovate.json:

{
  "pnpm-catalog": {
    "enabled": true
  }
}

For related pnpm monorepo issues see pnpm workspace not working and pnpm peer dependency error. For npm-style dependency churn see npm warn deprecated. For bundler integration issues across monorepo packages see Webpack HMR not working. For type resolution failures that look like catalog problems but turn out to be tsconfig issues see TypeScript cannot find module.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles