facebook/flow · error · NodeIsMutatedError
Attempted to mutate a `${node.type}.${key}` when it has alre
Error message
Attempted to mutate a `${node.type}.${key}` when it has already been mutated. What it means
MutationContext also tracks which non-array keys of each node have already been mutated; assertNotMutated throws NodeIsMutatedError when the same node.key is written twice within a single transform. Scalar keys must keep a single authoritative value, so a second write is treated as a logic bug (array keys are tracked in a separate map and tolerated, since concurrent array edits are considered safe).
Source
Thrown at packages/flow-transform/src/transform/MutationContext.js:74
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
- Consolidate both writes into one mutation that produces the final value
- Handle each node in exactly one visitor so the second never runs
- Run the two changes as two separate transform passes on fresh ASTs instead of one pass
Example fix
// before visitorA replaces fn.body; visitorB also replaces fn.body; // second write to same key -> throws // after visitorB replaces fn.body and includes visitorA's edits in the replacement;
Defensive patterns
Strategy: validation
Validate before calling
// dedupe queued mutations per node before applying
const seen = new Set();
mutations = mutations.filter(m => {
if (seen.has(m.target)) return false; // one write per node per pass
seen.add(m.target);
return true;
}); Type guard
const alreadyMutated = (touched, node, key) => (touched.get(node) || new Set()).has(key);
Try / catch
try {
mutationContext.markMutation(node, key);
} catch (e) {
if (e.constructor.name === 'NodeIsMutatedError') return; // skip duplicate write, first one wins
throw e;
} Prevention
- Ensure exactly one visitor handles a given node.key per pass (return the node to stop propagation)
- Merge overlapping edits into a single replacement value
- Run independent changes as separate transform passes on fresh ASTs
When it happens
Trigger: Two visitors (or two queued mutations) in one pass both writing the same singular property of the same node, e.g. replacing a function's body twice, or setting a declarator's init and then replacing the declarator's init again.
Common situations: Composing independent codemods that each touch the same property; a generic visitor plus a special-case visitor both firing for one node because the special case returns the node instead of stopping propagation.
Related errors
- Attempted to mutate a `${node.type}.${key}` on a deleted nod
- 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/56b64c051a12c429.
Report an issue: GitHub.