{"record":{"id":"d10f4c9932a82825","repo":"payloadcms/payload","slug":"circular-reference-detected-the-parent-chain-cont","errorCode":null,"errorMessage":"Circular reference detected: the parent chain contains a loop","messagePattern":"Circular reference detected: the parent chain contains a loop","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/payload/src/hierarchy/hooks/collectionBeforeChange.ts","lineNumber":96,"sourceCode":"      ? collection.hierarchy.parentFieldName\n      : undefined\n\n  if (!parentFieldName) {\n    return\n  }\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,","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/hierarchy/hooks/collectionBeforeChange.ts#L78-L114","documentation":"Thrown by validateNoCircularReference.checkAncestor when, while walking up the parent chain from the proposed parent, it revisits a node already in the visited set. That indicates a true loop in the chain (A->B->A) rather than a legitimate move into a child.","triggerScenarios":"Updating a document's parent to an ancestor whose chain, after the update, would loop back to the starting node — typically caused by direct DB edits, concurrent updates, or a previously corrupted chain.","commonSituations":"Data imported with corrupt parent links; two concurrent updates racing on the same subtree; manual SQL that created a cycle the hook now blocks further edits on.","solutions":["Inspect and repair the existing chain with a one-off script before retrying the update.","Serialise hierarchy reparenting writes (lock or queue) to prevent races.","Use the afterChange reparenting path instead of manually reassigning parents across a subtree.","Add a maintenance job that detects and reports cycles in the parent column."],"exampleFix":"// before — direct DB edit created A->B->A, now any update throws\nawait payload.update({ collection: 'folders', id: bId, data: { parent: aId } })\n\n// after — repair first, then edit\n// 1. fix the corrupt link via a script that walks parent once\nawait payload.db.updateOne({ collection: 'folders', id: aId, data: { parent: null } })\n// 2. retry the intended update\nawait payload.update({ collection: 'folders', id: bId, data: { parent: aId } })","handlingStrategy":"try-catch","validationCode":"// Walk the parent chain once; if any id repeats, repair before saving\nconst seen = new Set<string>()\nlet cursor = proposedParentId\nwhile (cursor) {\n  if (seen.has(String(cursor))) throw new Error('cycle in existing chain')\n  seen.add(String(cursor))\n  cursor = await getParentOf(cursor)\n}","typeGuard":"function hasCycle(chain: string[]): boolean {\n  return new Set(chain).size !== chain.length\n}","tryCatchPattern":"try {\n  await payload.update({ collection, id, data })\n} catch (err) {\n  if (err instanceof Error && /Circular reference detected/.test(err.message)) {\n    // surface to user; offer a repair flow\n  }\n  throw err\n}","preventionTips":["Never write parent links via raw SQL; always go through the Local API.","Serialise hierarchy reparenting writes to prevent concurrent cycles."],"tags":["hierarchy","validation","before-change","circular","data-integrity"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}