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

  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.