Fix: pnpm Catalog Protocol Not Working — Cannot Find Catalog, Resolution Errors, and Lockfile Issues
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 specOr 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.1Workspace 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.8I 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 installIf 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-lockfilethen 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:
| Tool | Status | Workaround |
|---|---|---|
| TypeScript | Works (resolves from node_modules, not package.json) | none |
| ESLint | Works for most rules | none for typical config |
| Renovate | Native catalog manager since 38.x | enable pnpm-catalog manager |
| Dependabot | Partial: opens PRs but may misread cataloged versions | review PR diffs by hand |
| npm audit / yarn audit | Refuses to parse catalog: references | run pnpm audit instead |
| IDE language servers | Mostly fine via node_modules | restart language server after install |
| Older bundlers and codegen tools | May read package.json directly | upgrade 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 reactThis 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 catalogsThis 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.0Step 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: pnpm Workspace Not Working — workspace:* Protocol, Catalog, Filters, and Hoisting Issues
How to fix pnpm workspace errors — workspace:* not resolving, catalog versions out of sync, --filter not matching, peer deps unmet across packages, shamefully-hoist trade-offs, and publishConfig for releases.
Fix: Node.js Test Runner Not Working — node --test, TypeScript, Mocks, Coverage, and Watch Mode
How to fix Node.js built-in test runner errors — node --test not finding files, ESM vs CJS imports, TypeScript with --experimental-strip-types, mock.method isolation, coverage reporting, and watch mode setup.
Fix: Nx Not Working — project.json Targets, Affected Commands, Caching, and Generators
How to fix Nx errors — nx.json plugin config, project.json target inputs/outputs, nx affected base branch, cache misses, generator schema, custom executors, and nx migrate failures.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.