facebook/flow · error · InvalidReplacementError
Expected to find the ${target.type} as a direct child of the
Error message
Expected to find the ${target.type} as a direct child of the ${parent.type}. What it means
replaceNodeMutation resolves the target's location by scanning the parent's visitor keys, checking both direct identity and getOriginalNode(...) so that detached (wrapper) nodes whose original matches the target are found. If no key or array slot holds the target, this InvalidReplacementError is thrown: the flow-transform counterpart of the 'not a direct child' invariant, indicating the parent link is stale or the target was already replaced or detached.
Source
Thrown at packages/flow-transform/src/transform/mutations/ReplaceNode.js:109
const child = (parent as $FlowFixMe)[key];
if (isNode(child)) {
// $FlowFixMe[invalid-compare]
if (child === target) {
return {type: 'single', parent, key};
}
} else if (Array.isArray(child)) {
for (let i = 0; i < child.length; i += 1) {
const current = child[i];
const originalNode = getOriginalNode(current);
if (current === target || originalNode === target) {
return {type: 'array', parent, key, targetIndex: i};
}
}
}
}
// this shouldn't happen ever
throw new InvalidReplacementError(
`Expected to find the ${target.type} as a direct child of the ${parent.type}.`,
);
}
View on GitHub (pinned to d1341dac89)
Solutions
- Replace each node at most once per pass; check your queued mutations for duplicate targets before applying
- Perform mutations during the traversal that found the nodes, not from a cached list afterwards
- Re-run traversal on the mutated AST if you need a second round of replacements
Example fix
// before
const seen = collectTargets(ast);
for (const t of seen) replaceNodeMutation(t, newNode); // second replace of same t throws
// after
const done = new Set();
for (const t of collectTargets(ast)) {
if (done.has(t)) continue;
done.add(t);
replaceNodeMutation(t, newNode);
} Defensive patterns
Strategy: try-catch
Validate before calling
const replaced = new Set();
function safeReplace(target, replacement) {
if (replaced.has(target)) return; // one replacement per node per pass
replaced.add(target);
mutations.push(replaceNodeMutation(target, replacement));
} Type guard
const isDirectChildOf = (parent, target) =>
Object.keys(parent).some(k =>
parent[k] === target ||
(Array.isArray(parent[k]) &&
parent[k].some(c => c === target || (c && c.original === target)))
); Try / catch
try {
mutations.push(replaceNodeMutation(target, replacement));
} catch (e) {
if (e.message.includes('as a direct child')) {
throw new Error('Target is no longer attached (already replaced?): re-traverse before replacing');
}
throw e;
} Prevention
- Replace each node at most once per pass; dedupe queued replacements by target
- Issue mutations during traversal, not from node lists cached before mutation
- Keep detached-node wrappers and originals consistent: do not mix raw nodes with wrapped ones
When it happens
Trigger: Calling replaceNodeMutation on a node that was already replaced earlier in the same pass; replacing a node whose parent pointer references a tree that was concurrently modified; mixing raw original nodes with detached-node wrappers from different transform runs.
Common situations: Queueing multiple replacements for the same node; caching nodes between the traversal and mutation phases; reusing detached-node wrappers built against an older AST.
Related errors
- Could not find target in array of `${node.parent.type}.${key
- import/export cannot be replaced into a ${replacementParent.
- Expected to find the ${target.type} as a direct child of the
- Cannot insert array into non-array parent type: ${parent.typ
- Attempted to mutate a `${node.type}.${key}` on a deleted nod
AI-assisted analysis of facebook/flow@d1341dac89 (2026-08-17).
Data as JSON: /api/errors/bbf1edd71d448789.
Report an issue: GitHub.