facebook/flow · error · InvalidReplacementError

Expected to find the

Error message

Expected to find the ${target.type} as a direct child of the ${parent.type}.

What it means

InvalidReplacementError thrown by flow-transform's replaceNode when locating the target: it walks every key of the parent and looks for the target as a direct child (by identity, or via getOriginalNode for detached nodes). If the target is not a direct child of its parent - it is deeper in the tree, already replaced, or a stale node object - the lookup fails and throws.

Solutions

  1. Call replaceNode on the node itself (the mutation derives the parent) and only once per node per run.
  2. After any replace, continue mutating from the replacement node, not the original.
  3. Use `replaceNodeWithContext` when you need the surrounding context to stay consistent.
  4. Never reuse node objects across separate transform() invocations.

Example fix

// before
mutate.replaceNode(oldTarget, replacement); // oldTarget already replaced earlier - throws

// after - mutate the current tree's node
mutate.replaceNode(currentTarget, replacement);
Defensive patterns

Strategy: validation

Validate before calling

function isDirectChild(parent, target) {
  return Object.values(parent).some(v =>
    v === target || (Array.isArray(v) && v.includes(target)),
  );
}

if (isDirectChild(node.parent, node)) {
  mutate.replaceNode(node, replacement);
}

Try / catch

try {
  mutate.replaceNode(node, replacement);
} catch (e) {
  if (e.name === 'InvalidReplacementError' && /Expected to find/.test(e.message)) {
    // node is stale (already replaced) - re-run the traversal on current AST
  } else throw e;
}

Prevention

When it happens

Trigger: `mutate.replaceNode(node, replacement)` after an earlier mutation in the same run replaced or removed `node` or its parent; passing a node whose `parent` pointer points at a different (old) subtree; replacing a node captured from a previous transform() call.

Common situations: Chained codemod operations where a replace happens on a node that a previous visitor already replaced; holding onto nodes between separate transform runs; replacing a node found via scope analysis after the tree was mutated.

Related errors


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

Appendix: source

Thrown at packages/flow-transform/src/transform/mutations/ReplaceNode.js:109

    const child = (parent as $FlowFixMe)[key];
    if (isNode(child)) {
      // $FlowFixMe[invalid-compare]
      if (child === target) {
        return {type: 'single', parent, key};
      }
    } else if (Array.isArray(child)) {
      for (let i = 0; i < child.length; i += 1) {
        const current = child[i];
        const originalNode = getOriginalNode(current);
        if (current === target || originalNode === target) {
          return {type: 'array', parent, key, targetIndex: i};
        }
      }
    }
  }

  // this shouldn't happen ever
  throw new InvalidReplacementError(
    `Expected to find the ${target.type} as a direct child of the ${parent.type}.`,
  );
}

View on GitHub (pinned to f88ac94bcf)