facebook/flow · error · NodeIsDeletedError
Attempted to mutate a `${node.type}.${key}` on a deleted nod
Error message
Attempted to mutate a `${node.type}.${key}` on a deleted node. What it means
MutationContext keeps a Set of every node marked deleted (via markDeletion, called by remove and replace mutations, which delete the whole subtree). markMutation runs assertNotDeleted first, so attempting any further mutation on a deleted node throws NodeIsDeletedError with this message. It exists because mutating a detached subtree produces output that silently disappears from the printed code.
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 d1341dac89)
Solutions
- Issue exactly one mutation per node per transform pass
- When replacing a statement, fold the child-level changes into the replacement nodes instead of mutating the original afterwards
- Keep your own Set of nodes you have removed or replaced and skip them when applying later mutations
Example fix
// before mutations.push(replaceWithMany(stmt, [a, b])); mutations.push(mutate(stmt.body, 'key', v)); // stmt already deleted -> throws // after mutations.push(replaceWithMany(stmt, [a, b])); // single mutation owns the node
Defensive patterns
Strategy: validation
Validate before calling
// keep your own ledger of nodes you have removed or replaced this pass
const deleted = new Set();
function safePush(mutation) {
if (deleted.has(mutation.target)) return; // skip stale mutations
deleted.add(mutation.target);
mutations.push(mutation);
} Type guard
const isNodeDeleted = (mutationContext, node) => mutationContext._deletedNodes.has(node); // private field; prefer your own ledger
Try / catch
try {
mutationContext.markMutation(node, key);
} catch (e) {
if (e.constructor.name === 'NodeIsDeletedError') return; // skip mutations on removed subtrees
throw e;
} Prevention
- Apply at most one mutation per node per transform pass
- Fold child-level edits into the replacement nodes instead of mutating afterwards
- Track removed and replaced nodes in a Set and skip queued mutations that target them
When it happens
Trigger: Within one transform pass: a visitor first replaces a statement (ReplaceStatementWithMany marks the original deleted) and then another mutation anchors on that same statement or tries to write one of its keys; queueing several mutations against the same target and flushing them together.
Common situations: Codemods that both rewrite a statement and separately mutate its children; visitor libraries that collect mutations during traversal and apply them in a batch, letting two operations touch one node.
Related errors
- Attempted to mutate a `${node.type}.${key}` when it has alre
- Attempted to insert a statement into `${parentWithType.type}
- Could not find target in array of `${parent.type}.${key}`.
- Expected to find a valid statement parent, but found a paren
- Expected to find the target "${target.type}" on the "${resul
AI-assisted analysis of facebook/flow@d1341dac89 (2026-08-17).
Data as JSON: /api/errors/141977bb42b89aef.
Report an issue: GitHub.