facebook/flow · error · InvalidStatementError

Could not find target in array of `${parent.type}.${key}`.

Error message

Could not find target in array of `${parent.type}.${key}`.

What it means

Thrown by getAssertedIndex() inside getStatementParent() when the target's parent is a BlockStatement, Program, or SwitchCase but target is not present in the parent's body/consequent array (indexOf returns -1). This means the node is detached or stale: its .parent pointer references a container whose array no longer contains it, usually because the node was already removed or the array was mutated without keeping the parent link in sync. Statement mutations (InsertStatement, RemoveStatement, ReplaceStatementWithMany) require the target to be findable in its container to compute the insertion/removal index.

Source

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

    for (const key of invalidKeys) {
      // $FlowExpectedError[prop-missing]
      const value = parentWithType[key];

      if (
        // $FlowFixMe[invalid-compare]
        value === target ||
        (Array.isArray(value) && value.includes(target))
      ) {
        throw new InvalidStatementError(
          `Attempted to insert a statement into \`${parentWithType.type}.${key}\`.`,
        );
      }
    }
  }
  function getAssertedIndex(key: string, arr: ReadonlyArray<unknown>): number {
    const idx = arr.indexOf(target);
    if (idx === -1) {
      throw new InvalidStatementError(
        `Could not find target in array of \`${parent.type}.${key}\`.`,
      );
    }
    return idx;
  }

  const parent = target.parent;
  const result: StatementParent = (() => {
    switch (parent.type) {
      case 'IfStatement': {
        assertValidStatementLocation(parent, 'test');
        const key = parent.consequent === target ? 'consequent' : 'alternate';
        return {type: 'single', parent, key};
      }

      case 'LabeledStatement': {
        assertValidStatementLocation(parent, 'label');
        return {type: 'single', parent, key: 'body'};

View on GitHub (pinned to d1341dac89)

Solutions

  1. Re-traverse (or re-collect the node references) immediately before applying each mutation so targets are fresh.
  2. When batching mutations, filter out any mutation whose target is no longer present in its parent array after earlier mutations run.
  3. Never manually splice body/consequent arrays; express every change as a library mutation so parent links stay consistent.
  4. Order mutations so that a remove/replace of a node never precedes another mutation that references the same node.

Example fix

// before
const mutations = collectFromTraversal(ast); // stale references
applyAll(mutations); // second mutation targets a node removed by the first

// after
for (const mutation of collectFromTraversal(ast)) {
  applyMutation(ast, mutation);          // one mutation per pass
  ast = reparseOrRetraverse(ast);        // refresh references before the next
}
Defensive patterns

Strategy: validation

Validate before calling

function isStillInParentArray(node) {
  const parent = node.parent;
  if (parent == null) return false;
  const arr = parent.type === 'SwitchCase' ? parent.consequent : parent.body;
  return Array.isArray(arr) && arr.includes(node);
}

const safeMutations = mutations.filter(m => isStillInParentArray(m.node ?? m.target));

Type guard

/** True when the node is still reachable from its declared array container. */
function isAttachedArrayMember(node) {
  const parent = node.parent;
  if (parent == null) return false;
  if (parent.type === 'SwitchCase') return parent.consequent.includes(node);
  if (parent.type === 'BlockStatement' || parent.type === 'Program') {
    return parent.body.includes(node);
  }
  return false;
}

Try / catch

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

for (const mutation of mutations) {
  try {
    applyMutation(ast, mutation);
  } catch (err) {
    if (err instanceof InvalidStatementError && err.message.startsWith('Could not find target')) {
      continue; // target already removed by an earlier mutation; skip
    }
    throw err;
  }
}

Prevention

When it happens

Trigger: Applying a second mutation whose target was already removed by an earlier mutation in the same batch; splicing parent.body manually (body.splice(i, 1)) without clearing node.parent and then calling a mutation on the removed node; holding node references from a traversal done before earlier mutations were applied.

Common situations: Batching multiple mutations computed from a single pre-mutation traversal, where two mutations touch overlapping nodes; mixing manual AST edits with the library's mutation API; reusing node references across transform passes.

Related errors


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