{"record":{"id":"1fd128d6abbc14d7","repo":"Automattic/mongoose","slug":"infinite-subdocument-loop-subdoc-with-id-doc","errorCode":null,"errorMessage":"Infinite subdocument loop: subdoc with _id ${doc._id} is a parent of itself","messagePattern":"Infinite subdocument loop: subdoc with _id (.+?) is a parent of itself","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/helpers/document/cleanModifiedSubpaths.js","lineNumber":37,"sourceCode":"      if (schemaType?.$isMongooseDocumentArray) {\n        continue;\n      }\n    }\n    if (modifiedPath.startsWith(path + '.')) {\n      doc.$__.activePaths.clearPath(modifiedPath);\n      ++deleted;\n\n      if (doc.$isSubdocument) {\n        cleanParent(doc, modifiedPath);\n      }\n    }\n  }\n  return deleted;\n};\n\nfunction cleanParent(doc, path, seen = new Set()) {\n  if (seen.has(doc)) {\n    throw new Error('Infinite subdocument loop: subdoc with _id ' + doc._id + ' is a parent of itself');\n  }\n  const parent = doc.$parent();\n  const newPath = doc.$__pathRelativeToParent(void 0, false) + '.' + path;\n  parent.$__.activePaths.clearPath(newPath);\n  if (parent.$isSubdocument) {\n    cleanParent(parent, newPath, seen);\n  }\n}\n","sourceCodeStart":19,"sourceCodeEnd":46,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/helpers/document/cleanModifiedSubpaths.js#L19-L46","documentation":"When modified subdocument paths are cleaned (cleanModifiedSubpaths, run during save()/removal bookkeeping), Mongoose walks up each subdocument's $parent() chain to clear modified paths on ancestors. A seen Set detects when the walk revisits the same subdocument - meaning the subdoc is its own ancestor, a cyclic subdocument hierarchy - and throws Error('Infinite subdocument loop: subdoc with _id ... is a parent of itself') instead of recursing forever.","triggerScenarios":"Assigning a subdocument (or an array containing it) into its own subtree, e.g. doc.children[0].children = doc.children or sub.field = sub; reusing one subdocument instance at multiple depths of the same document; then triggering save()/pull/remove so cleanModifiedSubpaths and cleanParent run.","commonSituations":"Building trees in memory by linking existing subdoc instances; copy logic that assigns references instead of cloning; aliasing one subdoc instance into sibling branches of the same parent document.","solutions":["Break the cycle: remove self-referencing assignments - a subdoc instance must have exactly one parent chain.","Clone before nesting: assign subdoc.toObject() (or a fresh instance) instead of the live instance.","Model recursive structures with references (ObjectId + populate) instead of embedded self-nesting."],"exampleFix":"// before\nconst cat = doc.categories[0];\ncat.subcategories = doc.categories; // the array contains cat itself\nawait doc.save(); // Error: subdoc is a parent of itself\n\n// after (clone the data instead of aliasing the instances)\nconst cat = doc.categories[0];\ncat.subcategories = doc.categories.map(c => c.toObject());\nawait doc.save();","handlingStrategy":"validation","validationCode":"// reject cyclic subdocument graphs before saving\nfunction isOwnAncestor(doc) {\n  const seen = new Set();\n  let p = doc.$parent();\n  while (p != null) {\n    if (p === doc) return true;\n    if (seen.has(p)) return true;\n    seen.add(p);\n    p = p.$isSubdocument ? p.$parent() : null;\n  }\n  return false;\n}\nif (doc.$isSubdocument && isOwnAncestor(doc)) {\n  throw new Error('cyclic subdocument hierarchy: subdoc is its own ancestor');\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never assign a live subdocument instance into its own subtree; clone with toObject() first.","Construct nested data from plain objects and let Mongoose instantiate subdocs exactly once.","For recursive or unbounded structures use refs + populate instead of embedding."],"tags":["mongoose","subdocument","circular-reference","save","infinite-loop"],"backgroundTag":"circular-reference","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}