Is there an existing issue for this?
This issue exists in the latest npm version
This is not just a request to bump a dependency for a CVE
Current Behavior
npm install -g of a package whose peer group resolves across multiple prerelease lines crashes with a TypeError from Arborist instead of installing (or reporting a peer conflict):
$ npm install -g --prefix /tmp/npm-repro @deepseek-harness-tui/dsh-tui@0.9.0
npm error Cannot read properties of null (reading 'children')
Verbose stack (npm 12.0.2 / @npmcli/arborist 10.0.2):
501 verbose stack TypeError: Cannot read properties of null (reading 'children')
501 verbose stack at new PlaceDep (.../node_modules/npm/node_modules/@npmcli/arborist/lib/place-dep.js:306:32)
501 verbose stack at #buildDepStep (.../node_modules/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js:1145:18)
501 verbose stack at async Arborist.buildIdealTree (.../node_modules/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js:188:7)
501 verbose stack at async Arborist.reify (.../node_modules/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js:118:5)
501 verbose stack at async Install.exec (.../node_modules/npm/node_modules/npm/lib/commands/install.js:176:5)
On npm 11.19.0 the same crash occurs at the same lines in arborist 9.x.
Expected Behavior
No crash. If the peer set cannot be placed, npm should either complete the install (placing the conflicting peers deeper) or emit a proper ERESOLVE peer-conflict error with the usual --force/--legacy-peer-deps guidance.
Steps To Reproduce
- Use npm 11.19.0 or 12.0.2 (latest). No project needed; an empty global prefix is enough:
mkdir -p /tmp/npm-repro/lib/node_modules
npm install -g --prefix /tmp/npm-repro @deepseek-harness-tui/dsh-tui@0.9.0
- Observe the
TypeError: Cannot read properties of null (reading 'children') crash. Nothing gets installed.
Notes on the trigger: @deepseek-harness-tui/dsh-tui@0.9.0 declares 26 required peerDependencies with dual ranges such as ^0.1.0-rc.6 || ^0.1.1-rc.1, while its regular dependencies pin ^0.1.0-rc.8 of the same packages and their dist-tags point at older lines (latest = 0.1.0-rc.6, next = 0.1.1-rc.2). Mixing those prerelease lines inside one peer set is what trips the bug. The previous release 0.8.6, whose peers use single ranges (^0.1.0-rc.7), installs fine on the same npm versions, so the user-visible trigger is package shape, not user error.
Environment
- npm 12.0.2 (also reproduced on npm 11.19.0; also unfixed on the
latest branch as of today — place-dep.js still has the unguarded virtualRoot.children at the same spot)
- Node.js v24.19.0 (also v24.14.1)
- macOS 15 (Darwin 25.6.0); no OS-specific code is involved
- Commands:
npm install -g (crash also happens with --dry-run, --force, and --install-strategy=nested)
Anything else?
Root-cause analysis (happy to PR a fix if the direction below sounds right):
#loadPeerSet in build-ideal-tree.js already knows a node can be replaced and detached mid-loop; it guards with if (!node.parent) break (and the comment above it says a detached node needs no further processing) — but it then still returns that detached node.
#buildDepStep passes the returned (detached, parent === null) node to new PlaceDep({ edge, dep, ... }).
- In
place-dep.js, the PlaceDep constructor stores const virtualRoot = this.dep.parent (line 242 in npm 12.0.2) and later does virtualRoot.children.get(peerEdge.name) when placing the node's unmet peers (line 306) — null deref.
Possible fixes: have #loadPeerSet return null (or the replacement node) when detached and skip it in #buildDepStep, or add a null guard in PlaceDep around the peer-placement loop (treat missing virtualRoot as "no peers to place here"). The #loadPeerSet detachment comments mention npm/cli#9249 era fixes, so there may be prior art in how detached nodes are handled elsewhere.
Possibly related: #9787 (Cannot read properties of null (reading 'edgesOut') inside #loadPeerSet) looks like the same underlying "detached node escapes peer-set resolution" problem crashing at a different line; it is currently labeled Cannot Reproduce, and the deterministic repro above may help there too.
Workarounds I found, for other users hitting this: pinning every peer to one release line in the same npm i -g command works (e.g. all @deepseek-ai/dsh-* peers at 0.1.0-rc.8). --legacy-peer-deps avoids the crash but skips installing the peers entirely, which breaks this package at runtime since it imports them.
Is there an existing issue for this?
This issue exists in the latest npm version
This is not just a request to bump a dependency for a CVE
Current Behavior
npm install -gof a package whose peer group resolves across multiple prerelease lines crashes with aTypeErrorfrom Arborist instead of installing (or reporting a peer conflict):Verbose stack (npm 12.0.2 / @npmcli/arborist 10.0.2):
On npm 11.19.0 the same crash occurs at the same lines in arborist 9.x.
Expected Behavior
No crash. If the peer set cannot be placed, npm should either complete the install (placing the conflicting peers deeper) or emit a proper
ERESOLVEpeer-conflict error with the usual--force/--legacy-peer-depsguidance.Steps To Reproduce
TypeError: Cannot read properties of null (reading 'children')crash. Nothing gets installed.Notes on the trigger:
@deepseek-harness-tui/dsh-tui@0.9.0declares 26 requiredpeerDependencieswith dual ranges such as^0.1.0-rc.6 || ^0.1.1-rc.1, while its regular dependencies pin^0.1.0-rc.8of the same packages and their dist-tags point at older lines (latest=0.1.0-rc.6,next=0.1.1-rc.2). Mixing those prerelease lines inside one peer set is what trips the bug. The previous release0.8.6, whose peers use single ranges (^0.1.0-rc.7), installs fine on the same npm versions, so the user-visible trigger is package shape, not user error.Environment
latestbranch as of today —place-dep.jsstill has the unguardedvirtualRoot.childrenat the same spot)npm install -g(crash also happens with--dry-run,--force, and--install-strategy=nested)Anything else?
Root-cause analysis (happy to PR a fix if the direction below sounds right):
#loadPeerSetinbuild-ideal-tree.jsalready knows a node can be replaced and detached mid-loop; it guards withif (!node.parent) break(and the comment above it says a detached node needs no further processing) — but it then still returns that detached node.#buildDepSteppasses the returned (detached,parent === null) node tonew PlaceDep({ edge, dep, ... }).place-dep.js, thePlaceDepconstructor storesconst virtualRoot = this.dep.parent(line 242 in npm 12.0.2) and later doesvirtualRoot.children.get(peerEdge.name)when placing the node's unmet peers (line 306) — null deref.Possible fixes: have
#loadPeerSetreturnnull(or the replacement node) when detached and skip it in#buildDepStep, or add a null guard inPlaceDeparound the peer-placement loop (treat missingvirtualRootas "no peers to place here"). The#loadPeerSetdetachment comments mentionnpm/cli#9249era fixes, so there may be prior art in how detached nodes are handled elsewhere.Possibly related: #9787 (
Cannot read properties of null (reading 'edgesOut')inside#loadPeerSet) looks like the same underlying "detached node escapes peer-set resolution" problem crashing at a different line; it is currently labeledCannot Reproduce, and the deterministic repro above may help there too.Workarounds I found, for other users hitting this: pinning every peer to one release line in the same
npm i -gcommand works (e.g. all@deepseek-ai/dsh-*peers at0.1.0-rc.8).--legacy-peer-depsavoids the crash but skips installing the peers entirely, which breaks this package at runtime since it imports them.