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

  1. Consolidate both writes into one mutation that produces the final value
  2. Handle each node in exactly one visitor so the second never runs
  3. 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

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


AI-assisted analysis of facebook/flow@d1341dac89 (2026-08-17). Data as JSON: /api/errors/56b64c051a12c429. Report an issue: GitHub.