immerjs/immer · warning · Error

Cannot resolve path at

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.

Solutions

  1. 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).
  2. 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.
  3. If you must restructure the tree, re-obtain the nested draft from the root afterward so its path reflects the new structure.
  4. 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

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.

Appendix: 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)