facebook/flow · error · InvalidStatementError
Expected to find the target "${target.type}" on the "${resul
Error message
Expected to find the target "${target.type}" on the "${result.parent.type}.${result.key}", but found a different node. This likely means that you attempted to mutate around the target after it was deleted/replaced. What it means
Thrown by getStatementParent() as a final consistency check for 'single' slots: after computing which property of the parent should hold the target (e.g. IfStatement.consequent/alternate, loop body), it verifies result.parent[result.key] === target and finds a different node there. As the message states, this means you attempted to mutate around a target after it was deleted or replaced — the target still points at the old parent via .parent, but the slot now holds a new node. It is the single-child counterpart of the array-index check performed by getAssertedIndex().
Source
Thrown at packages/flow-transform/src/transform/mutations/utils/getStatementParent.js:142
key: 'body',
targetIndex: getAssertedIndex('body', parent.body),
};
}
}
throw new InvalidStatementError(
`Expected to find a valid statement parent, but found a parent of type "${parent.type}".`,
);
})();
if (
// array insertions are already validated by the getAssertedIndex function
result.targetIndex == null &&
// $FlowExpectedError[prop-missing]
// $FlowFixMe[invalid-compare]
result.parent[result.key] !== target
) {
throw new InvalidStatementError(
`Expected to find the target "${target.type}" on the "${result.parent.type}.${result.key}", but found a different node. ` +
'This likely means that you attempted to mutate around the target after it was deleted/replaced.',
);
}
return result;
}
View on GitHub (pinned to d1341dac89)
Solutions
- Apply one mutation per traversal pass, re-collecting targets after each pass, so no mutation references a replaced node.
- Before applying each queued mutation, re-verify that target.parent[key] === target for single-slot parents (or that the array still contains it) and drop the mutation otherwise.
- Express replacements through the library's mutation API so parent links are updated, instead of assigning parent.consequent/body directly.
- If a replace and an insert must touch the same region, retarget the insert at the replacement node.
Example fix
// before
parent.consequent = newBlock; // manual replace, old node keeps .parent
applyMutation(ast, {kind: 'insert_statement', target: oldNode, ...}); // throws
// after
applyMutation(ast, {kind: 'replace_statement_with_many', target: oldNode, statements: [...]},);
// then re-traverse before inserting around the new node Defensive patterns
Strategy: validation
Validate before calling
function stillOwnsSlot(node) {
const parent = node.parent;
if (parent == null) return false;
switch (parent.type) {
case 'IfStatement':
return parent.consequent === node || parent.alternate === node;
case 'LabeledStatement':
case 'WithStatement':
case 'DoWhileStatement':
case 'WhileStatement':
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
return parent.body === node;
default:
return true; // array containers validated separately
}
} Type guard
/** True when the single-slot parent still holds this exact node (not a replacement). */
function isCurrentSlotOwner(node) {
const parent = node.parent;
if (parent == null) return false;
const key = parent.type === 'IfStatement'
? (parent.consequent === node ? 'consequent' : 'alternate')
: 'body';
return parent[key] === node;
} Try / catch
import {InvalidStatementError} from 'flow-transform/src/transform/Errors';
try {
applyMutation(ast, mutation);
} catch (err) {
if (err instanceof InvalidStatementError && err.message.includes('deleted/replaced')) {
// stale target: re-traverse and rebuild this mutation against the new node
mutation = rebuildMutationFromTraversal(ast, mutation);
} else {
throw err;
}
} Prevention
- Apply mutations one per pass and re-traverse after each replace/remove.
- Drop queued mutations whose target no longer owns its parent slot.
- Never assign parent.consequent/parent.body directly; use the mutation API.
When it happens
Trigger: Batching two mutations where the first replaces an if-consequent (or loop body) and the second inserts/removes around the old node; manually assigning parent.consequent = newNode while the old node keeps .parent pointing at that IfStatement, then calling a mutation on the old node.
Common situations: Codemod pipelines that compute a mutation list from one traversal and apply them in order, with a replace earlier in the list invalidating a later insertion target; mixing manual property assignment with the mutation API; retrying a failed mutation against an already-replaced node.
Related errors
- Could not find target in array of `${parent.type}.${key}`.
- Attempted to insert a statement into `${parentWithType.type}
- Expected to find a valid statement parent, but found a paren
- Invalid Mutation: Tried to mutate an elements array with an
- Expected parent node to be set on "${target.type}"
AI-assisted analysis of facebook/flow@d1341dac89 (2026-08-17).
Data as JSON: /api/errors/6b17a6c136834b56.
Report an issue: GitHub.