payloadcms/payload · error · Error
Circular reference detected: the parent chain contains a loo
Error message
Circular reference detected: the parent chain contains a loop
What it means
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.
Source
Thrown at packages/payload/src/hierarchy/hooks/collectionBeforeChange.ts:96
? collection.hierarchy.parentFieldName
: undefined
if (!parentFieldName) {
return
}
const fieldName = parentFieldName
async function checkAncestor(
ancestorId: number | string,
visitedNodes: Set<string> = new Set(),
): Promise<void> {
// Create unique key for this node
const nodeKey = `${collection.slug}:${ancestorId}`
// Check if we've visited this node before (true loop in the chain)
if (visitedNodes.has(nodeKey)) {
throw new Error(`Circular reference detected: the parent chain contains a loop`)
}
// If we've reached the current document, this means we're trying to move into a child
if (ancestorId === currentDocId) {
throw new Error('Cannot move folder into its own subfolder')
}
// Add this node to visited set
visitedNodes.add(nodeKey)
try {
const ancestor = (await req.payload.findByID({
id: ancestorId,
collection: collection.slug,
depth: 0,
req,
select: {
[fieldName]: true,View on GitHub (pinned to 00c58b35c0)
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.
Example fix
// before — direct DB edit created A->B->A, now any update throws
await payload.update({ collection: 'folders', id: bId, data: { parent: aId } })
// after — repair first, then edit
// 1. fix the corrupt link via a script that walks parent once
await payload.db.updateOne({ collection: 'folders', id: aId, data: { parent: null } })
// 2. retry the intended update
await payload.update({ collection: 'folders', id: bId, data: { parent: aId } }) Defensive patterns
Strategy: try-catch
Validate before calling
// Walk the parent chain once; if any id repeats, repair before saving
const seen = new Set<string>()
let cursor = proposedParentId
while (cursor) {
if (seen.has(String(cursor))) throw new Error('cycle in existing chain')
seen.add(String(cursor))
cursor = await getParentOf(cursor)
} Type guard
function hasCycle(chain: string[]): boolean {
return new Set(chain).size !== chain.length
} Try / catch
try {
await payload.update({ collection, id, data })
} catch (err) {
if (err instanceof Error && /Circular reference detected/.test(err.message)) {
// surface to user; offer a repair flow
}
throw err
} Prevention
- Never write parent links via raw SQL; always go through the Local API.
- Serialise hierarchy reparenting writes to prevent concurrent cycles.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Document cannot be its own parent
- Cannot move folder into its own subfolder
- Collection ${collectionSlug} is not a hierarchy
- The folder "${data.name || originalDoc.name}" contains ${isF
- The folder "${data?.name || originalDoc.name}" must have fol
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/d10f4c9932a82825.
Report an issue: GitHub.