facebook/flow · error · NodeIsDeletedError
${message}
Error message
${message} What it means
NodeIsDeletedError is flow-transform's fail-fast guard against mutating an AST node that a previous mutation in the same transform run already deleted or replaced. `MutationContext.markDeletion` puts nodes in a `_deletedNodes` set; `assertNotDeleted` (called from `markMutation` and from insert/replace operations) throws with the caller-supplied message, typically "Attempted to mutate a `<type>.<key>` on a deleted node." or "Attempted to insert before/after a deleted node".
Source
Thrown at packages/flow-transform/src/transform/MutationContext.js:65
`Attempted to mutate a \`${node.type}.${key}\` when it has already been mutated.`,
);
const map = Array.isArray(
// $FlowExpectedError[prop-missing]
node[key],
)
? this._mutatedArrays
: this._mutatedKeys;
map.set(node, map.get(node)?.add(key) ?? new Set([key]));
}
/**
* Throws if the node has been deleted
*/
assertNotDeleted(node: ESNode, message: string): void {
if (this._deletedNodes.has(node)) {
throw new NodeIsDeletedError(message);
}
}
/**
* Throws if the key of the node has been mutated
*/
assertNotMutated(node: ESNode, key: string, message: string): void {
if (this._mutatedKeys.get(node)?.has(key) === true) {
throw new NodeIsMutatedError(message);
}
}
appendCommentToSource(comment: Comment, placement: CommentPlacement): void {
this.code = appendCommentToSource(this.code, comment, placement);
}
}
View on GitHub (pinned to f88ac94bcf)
Solutions
- Issue at most one mutation per node per transform run; delete last after reads.
- After a replace, use the replacement node for any follow-up mutations, never the original.
- If you need remove-then-insert semantics, use a single `replaceStatementWithMany` with the full new statement list instead.
- Do not return mutations from both an enter and an `:exit` handler for the same selector.
Example fix
// before - two mutations touch the same node
if (path.node.type === 'ExpressionStatement') {
mutate.removeNode(path.node);
mutate.insertStatementBefore(path.node, buildLog()); // throws NodeIsDeletedError
}
// after - single replace mutation
mutate.replaceStatementWithMany(path.node, [buildLog()]); Defensive patterns
Strategy: validation
Validate before calling
// track nodes you have already retired in this run
const retired = new Set();
function safeRemove(mutate, node) {
if (retired.has(node)) return; // already deleted/replaced
retired.add(node);
mutate.removeNode(node);
}
function safeInsertBefore(mutate, anchor, stmt) {
if (retired.has(anchor)) return; // inserting around a deleted node throws
mutate.insertStatementBefore(anchor, stmt);
} Try / catch
try {
transform(code, visitors);
} catch (e) {
if (e.name === 'NodeIsDeletedError') {
// a later mutation touched a subtree an earlier mutation removed:
// find the two visitors fighting over the same node and reorder/dedupe
throw new Error(`codemod conflict: ${e.message}`, {cause: e});
}
throw e;
} Prevention
- One mutation per node per transform run; track retired nodes in a Set.
- Never return mutations from both enter and :exit handlers of the same selector.
- After a replace, mutate the replacement node, not the original.
- Prefer a single replaceStatementWithMany over remove-then-insert sequences.
When it happens
Trigger: Within one transform() run: `mutate.removeNode(stmt)` followed by another mutation that touches the same node or its subtree - e.g. insertStatementBefore on the removed statement, replaceNode on one of its descendants, or two visitors (enter and `:exit`) both returning mutations for the same node.
Common situations: Writing codemods with flow-transform/flow-upgrade: collecting nodes during traversal and mutating them all afterwards (some get removed then mutated again); a replace followed by an insert relative to the original node; visitors matching overlapping selectors that both try to delete/modify the same construct.
Related errors
- import/export cannot be inserted into a ${insertionParent.pa
- Tried to remove ${node.type} from parent of type ${node.pare
- Cannot perform a remove mutation on node of type ${node.type
- Could not find target in array of `${node.parent.type}.${key
- Expected to find the ${target.type} as a direct child of the
AI-assisted analysis of facebook/flow@f88ac94bcf (2026-08-20).
Data as JSON: /api/errors/3a6070ab0706bfab.
Report an issue: GitHub.