immerjs/immer · warning · Error
Cannot resolve path at '${path.join("/")}'
Error message
Cannot resolve path at '${path.join("/")}' What it means
Thrown inside the local resolvePath helper (src/plugins/patches.ts:116) during patch generation. It walks a draft's path segment by segment from the root copy_ via get(), and throws when an intermediate node is not traversable (typeof !== "object", or null). This is an internal control-flow sentinel: its only call site (src/plugins/patches.ts:107) sits inside try/catch and converts the throw into a null return from getPath, so it does not normally escape to a caller. It means a nested draft's location can no longer be reached from the root because the object graph was restructured.
Source
Thrown at src/plugins/patches.ts:122
try {
// Validate path can be resolved from ROOT
resolvePath(state.copy_, path)
} catch (e) {
return null // Path invalid
}
return path
}
// NEW: Add resolvePath helper function
function resolvePath(base: any, path: PatchPath): any {
let current = base
for (let i = 0; i < path.length - 1; i++) {
const key = path[i]
current = get(current, key)
if (!isObjectish(current) || current === null) {
throw new Error(`Cannot resolve path at '${path.join("/")}'`)
}
}
return current
}
const REPLACE = "replace"
const ADD = "add"
const REMOVE = "remove"
function generatePatches_(
state: ImmerState,
basePath: PatchPath,
scope: ImmerScope
): void {
if (state.scope_.processedForPatches_.has(state)) {
return
}
View on GitHub (pinned to d2c158f5ba)
Solutions
- Mutate nested values in place instead of replacing their parent container (set draft.items[0].value directly rather than reassigning draft.items and then editing an orphaned child).
- Do not capture a nested draft reference (const child = draft.a.b) and then restructure draft.a or draft before finishing the child's edit.
- If you must restructure the tree, re-obtain the nested draft from the root afterward so its path reflects the new structure.
- This throw is internally caught at src/plugins/patches.ts:108; if you see it escape, suspect a modified/patched Immer build or a direct call into getPath/resolvePath outside the library.
Example fix
// before: replacing the container orphans the nested draft, path no longer resolves
produce(state, draft => {
const child = draft.items[0] // nested draft captured
draft.items = [] // parent replaced -> child detached
child.value = 1 // path to child can't resolve from root
})
// after: mutate in place so the path stays valid
produce(state, draft => {
draft.items[0].value = 1
}) Defensive patterns
Strategy: validation
Validate before calling
// Before editing a nested draft captured earlier, confirm the path still
// resolves through object/map nodes from the root state.
import {isDraft} from "immer"
function pathStillResolves(root: any, path: Array<string | number>): boolean {
let cur: any = root
for (const seg of path) {
if (cur === null || typeof cur !== "object") return false
cur = cur instanceof Map ? cur.get(seg) : cur[seg as any]
}
return typeof cur === "object" && cur !== null
}
// usage: only edit the child if its container chain is intact
if (pathStillResolves(state, ["items", 0])) {
produce(state, draft => { draft.items[0].value = 1 })
} Type guard
// Narrow to a value that can still be descended into along a path. const isTraversable = (v: unknown): v is Record<PropertyKey, unknown> => v !== null && typeof v === "object"
Prevention
- Edit nested draft values in place; do not replace or truncate a container (draft.items = [], draft.a = null) while a draft of its descendant is still in use.
- Do not cache nested draft references across a restructuring operation - re-derive them from the root after the tree changes.
- Remember this throw is internally caught at src/plugins/patches.ts:108 and turned into a null path; if you observe odd/missing patches rather than a crash, suspect detached nested drafts.
- Keep producer functions short and local to the subtree they mutate so container lifetimes never overlap child edits.
When it happens
Trigger: getPath rebuilds a nested draft's path by walking parent_ links, then validates it against state.copy_ by calling resolvePath. The throw fires when an intermediate value along that path is a primitive, string, boolean, number, undefined, function, or null - i.e. a container in the path was replaced, truncated, nulled, or spliced after the child draft was created, leaving the stored path stale.
Common situations: Replacing or truncating a container after grabbing a nested draft (e.g. draft.items = [] while editing draft.items[0]); reassigning draft.a.b = null while holding a draft of draft.a.b.c; splicing an array that holds drafts; reassigning a Map key's value mid-edit. Because the throw is caught (patches.ts:108), the visible effect is that the draft's path is dropped - the patch is emitted at a different level or skipped - rather than a crash.
AI-assisted analysis of immerjs/immer@d2c158f5ba (2026-08-13).
Data as JSON: /api/errors/f67243a24c5edcb0.
Report an issue: GitHub.