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

  1. Issue at most one mutation per node per transform run; delete last after reads.
  2. After a replace, use the replacement node for any follow-up mutations, never the original.
  3. If you need remove-then-insert semantics, use a single `replaceStatementWithMany` with the full new statement list instead.
  4. 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

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


AI-assisted analysis of facebook/flow@f88ac94bcf (2026-08-20). Data as JSON: /api/errors/3a6070ab0706bfab. Report an issue: GitHub.