{"record":{"id":"192b539cdd2a42ac","repo":"payloadcms/payload","slug":"cannot-move-folder-into-its-own-subfolder","errorCode":null,"errorMessage":"Cannot move folder into its own subfolder","messagePattern":"Cannot move folder into its own subfolder","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/payload/src/hierarchy/hooks/collectionBeforeChange.ts","lineNumber":101,"sourceCode":"  }\n\n  const fieldName = parentFieldName\n\n  async function checkAncestor(\n    ancestorId: number | string,\n    visitedNodes: Set<string> = new Set(),\n  ): Promise<void> {\n    // Create unique key for this node\n    const nodeKey = `${collection.slug}:${ancestorId}`\n\n    // Check if we've visited this node before (true loop in the chain)\n    if (visitedNodes.has(nodeKey)) {\n      throw new Error(`Circular reference detected: the parent chain contains a loop`)\n    }\n\n    // If we've reached the current document, this means we're trying to move into a child\n    if (ancestorId === currentDocId) {\n      throw new Error('Cannot move folder into its own subfolder')\n    }\n\n    // Add this node to visited set\n    visitedNodes.add(nodeKey)\n\n    try {\n      const ancestor = (await req.payload.findByID({\n        id: ancestorId,\n        collection: collection.slug,\n        depth: 0,\n        req,\n        select: {\n          [fieldName]: true,\n        },\n      })) as JsonObject\n\n      const nextParent = ancestor?.[fieldName]\n","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/hierarchy/hooks/collectionBeforeChange.ts#L83-L119","documentation":"Thrown by validateNoCircularReference.checkAncestor when, walking up the proposed parent's chain, the traversal reaches the document being updated. Moving a node into one of its own descendants is rejected here rather than silently creating a loop.","triggerScenarios":"Updating a folder's parent to one of its own descendants (e.g. setting folder A's parent to folder C where C is a child of A).","commonSituations":"Drag-and-drop UI that allows dropping a folder into its own subfolder; bulk reparenting scripts that do not account for ancestry; misordered migration that reparents parents into children.","solutions":["On the client, disable drop targets that are descendants of the dragged node (compute ancestry first).","Before save, call getAncestors on the proposed parent and reject if it contains the current id.","For subtree moves, first detach the target from its parent (set parent null) then attach under the descendant.","Reorder migrations to reparent bottom-up, never parent-into-child."],"exampleFix":"// before\nawait payload.update({ collection: 'folders', id: aId, data: { parent: cId } }) // C is child of A\n\n// after\nconst ancestors = await getAncestors({ collectionSlug: 'folders', id: cId, req })\nif (ancestors.some(a => a.id === aId)) {\n  throw new Error('Cannot move into a descendant')\n}\nawait payload.update({ collection: 'folders', id: aId, data: { parent: cId } })","handlingStrategy":"validation","validationCode":"const ancestors = await getAncestors({ collectionSlug, id: proposedParentId, req })\nif (ancestors.some((a) => String(a.id) === String(currentDocId))) {\n  throw new Error('proposed parent is a descendant')\n}","typeGuard":"function isDescendant(\n  ancestors: { id: string | number }[],\n  selfId: string | number,\n): boolean {\n  return ancestors.some((a) => String(a.id) === String(selfId))\n}","tryCatchPattern":"try {\n  await payload.update({ collection, id, data })\n} catch (err) {\n  if (err instanceof Error && /own subfolder/.test(err.message)) {\n    return { error: 'Cannot move a folder into its own subfolder' }\n  }\n  throw err\n}","preventionTips":["Compute ancestry client-side and disable descendant drop targets.","Detach a node from its parent before moving it under a descendant."],"tags":["hierarchy","validation","before-change","circular"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}