can1357/oh-my-pi · error · Error
The new child is an ancestor of this node
Error message
The new child is an ancestor of this node
What it means
This mini-DOM's Node.appendChild enforces the same hierarchy invariant as the standard DOM: a node cannot be appended to itself or to one of its descendants (append would create a cycle, making parent chains infinite). The guard `node === this || node.contains(this)` detects that case and throws before any mutation, so the tree is left unchanged.
Source
Thrown at packages/utils/src/dom/core.ts:197
/** Text contained by this node. */
get textContent(): string | null {
return this.childNodes.map(child => child.textContent ?? "").join("");
}
set textContent(value: string | null) {
this.replaceChildren();
if (value) this.appendChild(this.documentForCreation().createTextNode(value));
}
/** Parent element, excluding document and fragments. */
get parentElement(): Element | null {
return this.parentNode instanceof Element ? this.parentNode : null;
}
/** Append a node, moving it from its old parent. */
appendChild<T extends Node>(child: T): T {
const node: Node = child;
if (node === this || node.contains(this)) throw new Error("The new child is an ancestor of this node");
if (child instanceof DocumentFragment) {
for (const nested of [...child.childNodes]) this.appendChild(nested);
return child;
}
child.parentNode?.removeChild(child);
child.parentNode = this;
child.setOwnerDocument(this.documentForCreation());
this.childNodes.push(child);
return child;
}
/** Insert a node before a current child, or append for null. */
insertBefore<T extends Node>(child: T, reference: Node | null): T {
if (reference === null) return this.appendChild(child);
const index = this.childNodes.indexOf(reference);
if (index < 0) throw new Error("The reference node is not a child of this node");
if (child instanceof DocumentFragment) {
for (const nested of [...child.childNodes]) this.insertBefore(nested, reference);View on GitHub (pinned to 9690622007)
Solutions
- Check the parent reference: it must be a node outside the child's subtree — log both nodes (tagName, parent chain) before appending.
- If you intended to re-parent (move) a node, first remove it, then append it to the genuinely new parent.
- To wrap `node` in `wrapper`, insert the wrapper at node's position first, then `wrapper.appendChild(node)` — never `node.appendChild(wrapper)`.
- Add a guard/assert in helper code that skips the append when `newParent.contains(child)` to fail loudly at your own callsite.
Example fix
// before: append to own descendant creates a cycle
node.parentNode.appendChild(wrapper); // wrapper === node's descendant? no — but if node is ancestor of target:
element.appendChild(element.parentNode); // throws
// after: verify the target is not inside the subtree
if (!element.contains(newParent)) {
newParent.appendChild(element);
} Defensive patterns
Strategy: validation
Validate before calling
function canAppend(parent: Node, child: Node): boolean {
return parent !== child && !parent.contains(child);
}
// call before: if (canAppend(newParent, node)) newParent.appendChild(node); Try / catch
try {
newParent.appendChild(node);
} catch (err) {
if (err instanceof Error && err.message === "The new child is an ancestor of this node") {
// fall back: insert at node's current position instead
node.parentNode?.insertBefore(newParent, node);
newParent.appendChild(node);
} else {
throw err;
}
} Prevention
- Before moving nodes, assert the destination is not inside the moved subtree (`!parent.contains(child)`).
- For wrapping operations, insert the wrapper at the node's position first, then move the node into it.
- Keep one direction of re-parenting logic; avoid generic 'attach node A to B' helpers without the cycle guard.
When it happens
Trigger: Calling `parent.appendChild(child)` where `child === parent`, or where `child` is any ancestor of `parent` (e.g. `element.parentNode.appendChild(element)` — a bug that moves an element under its own child). Also triggered indirectly through appendChild-based helpers: insertBefore(null path), append(), textContent setters, innerHTML assignments, and cloneNode re-parenting that walk through this code.
Common situations: Building trees bottom-up with a mistaken parent reference; wrapping an element with a new parent but accidentally re-appending the element to its own subtree; cloning logic that inserts the original instead of the clone; document restructuring loops where the 'new parent' variable still points at a node inside the moved subtree.
Related errors
- The reference node is not a child of this node
- The node to replace is not a child of this node
- The node to remove is not a child of this node
- tab.ariaSnapshot: selector ${__sel} matched no element
- Element handle selector no longer resolves
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/c39f550d117c43e5.
Report an issue: GitHub.