facebook/flow · error · InvalidStatementError

Expected to find the target "${target.type}" on the "${resul

Error message

Expected to find the target "${target.type}" on the "${result.parent.type}.${result.key}", but found a different node. This likely means that you attempted to mutate around the target after it was deleted/replaced.

What it means

Thrown by getStatementParent() as a final consistency check for 'single' slots: after computing which property of the parent should hold the target (e.g. IfStatement.consequent/alternate, loop body), it verifies result.parent[result.key] === target and finds a different node there. As the message states, this means you attempted to mutate around a target after it was deleted or replaced — the target still points at the old parent via .parent, but the slot now holds a new node. It is the single-child counterpart of the array-index check performed by getAssertedIndex().

Source

Thrown at packages/flow-transform/src/transform/mutations/utils/getStatementParent.js:142

          key: 'body',
          targetIndex: getAssertedIndex('body', parent.body),
        };
      }
    }

    throw new InvalidStatementError(
      `Expected to find a valid statement parent, but found a parent of type "${parent.type}".`,
    );
  })();

  if (
    // array insertions are already validated by the getAssertedIndex function
    result.targetIndex == null &&
    // $FlowExpectedError[prop-missing]
    // $FlowFixMe[invalid-compare]
    result.parent[result.key] !== target
  ) {
    throw new InvalidStatementError(
      `Expected to find the target "${target.type}" on the "${result.parent.type}.${result.key}", but found a different node. ` +
        'This likely means that you attempted to mutate around the target after it was deleted/replaced.',
    );
  }

  return result;
}

View on GitHub (pinned to d1341dac89)

Solutions

  1. Apply one mutation per traversal pass, re-collecting targets after each pass, so no mutation references a replaced node.
  2. Before applying each queued mutation, re-verify that target.parent[key] === target for single-slot parents (or that the array still contains it) and drop the mutation otherwise.
  3. Express replacements through the library's mutation API so parent links are updated, instead of assigning parent.consequent/body directly.
  4. If a replace and an insert must touch the same region, retarget the insert at the replacement node.

Example fix

// before
parent.consequent = newBlock;              // manual replace, old node keeps .parent
applyMutation(ast, {kind: 'insert_statement', target: oldNode, ...}); // throws

// after
applyMutation(ast, {kind: 'replace_statement_with_many', target: oldNode, statements: [...]},);
// then re-traverse before inserting around the new node
Defensive patterns

Strategy: validation

Validate before calling

function stillOwnsSlot(node) {
  const parent = node.parent;
  if (parent == null) return false;
  switch (parent.type) {
    case 'IfStatement':
      return parent.consequent === node || parent.alternate === node;
    case 'LabeledStatement':
    case 'WithStatement':
    case 'DoWhileStatement':
    case 'WhileStatement':
    case 'ForStatement':
    case 'ForInStatement':
    case 'ForOfStatement':
      return parent.body === node;
    default:
      return true; // array containers validated separately
  }
}

Type guard

/** True when the single-slot parent still holds this exact node (not a replacement). */
function isCurrentSlotOwner(node) {
  const parent = node.parent;
  if (parent == null) return false;
  const key = parent.type === 'IfStatement'
    ? (parent.consequent === node ? 'consequent' : 'alternate')
    : 'body';
  return parent[key] === node;
}

Try / catch

import {InvalidStatementError} from 'flow-transform/src/transform/Errors';

try {
  applyMutation(ast, mutation);
} catch (err) {
  if (err instanceof InvalidStatementError && err.message.includes('deleted/replaced')) {
    // stale target: re-traverse and rebuild this mutation against the new node
    mutation = rebuildMutationFromTraversal(ast, mutation);
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Batching two mutations where the first replaces an if-consequent (or loop body) and the second inserts/removes around the old node; manually assigning parent.consequent = newNode while the old node keeps .parent pointing at that IfStatement, then calling a mutation on the old node.

Common situations: Codemod pipelines that compute a mutation list from one traversal and apply them in order, with a replace earlier in the list invalidating a later insertion target; mixing manual property assignment with the mutation API; retrying a failed mutation against an already-replaced node.

Related errors


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