facebook/flow · error · InvalidRemovalError
Cannot perform a remove mutation on node of type ${node.type
Error message
Cannot perform a remove mutation on node of type ${node.type} What it means
removeNodeMutation is allowlisted per node type: it has explicit cases (statements, properties, params, Identifiers, Rest and SpreadElements, JSX attributes, ObjectType members) and every unlisted type falls through to this default InvalidRemovalError. The message names the offending node.type, which is the fastest diagnostic: that type has no supported removal semantics at all. This is by design: removal is only implemented where the result is guaranteed to remain valid, printable code.
Source
Thrown at packages/flow-transform/src/transform/mutations/RemoveNode.js:270
case 'OptionalCallExpression':
case 'CallExpression':
case 'NewExpression':
return 'arguments';
default:
throw new InvalidRemovalError(
getErrorMessage([
'ArrayExpression',
'ObjectExpression',
'CallExpression',
'OptionalCallExpression',
'NewExpression',
]),
);
}
default:
throw new InvalidRemovalError(
`Cannot perform a remove mutation on node of type ${node.type}`,
);
}
})();
const targetIndex = (() => {
// $FlowExpectedError[prop-missing]
const arr = node.parent[key];
const idx = arr.indexOf(node);
// $FlowFixMe[invalid-compare]
if (idx === -1) {
throw new InvalidRemovalError(
`Could not find target in array of \`${node.parent.type}.${key}\`.`,
);
}
return idx;
})();
View on GitHub (pinned to d1341dac89)
Solutions
- Use replaceNodeMutation (or replace-with) to swap the node for what should remain, instead of removing it
- If the goal is to drop a statement or property, remove at the level that is allowlisted (e.g. the VariableDeclarator rather than the Literal init)
- Check the switch in RemoveNode.js for the current allowlist before writing the codemod
Example fix
// before
mutations.push(removeNode(callNode)); // 'CallExpression' has no removal case
// after
mutations.push(replaceWith(callNode, {type: 'VoidLiteral'})); // or drop the enclosing statement Defensive patterns
Strategy: type-guard
Validate before calling
// mirror the allowlist in RemoveNode.js (keep in sync on upgrades)
const REMOVABLE_TYPES = new Set(['Property', 'SpreadElement', 'RestElement', 'Identifier', 'ImportDeclaration', 'JSXAttribute', 'ObjectTypeProperty', 'ObjectTypeSpreadProperty', 'ObjectTypeIndexer', 'ObjectTypeCallProperty', 'ObjectTypeInternalSlot']);
function isRemovable(node) {
return REMOVABLE_TYPES.has(node.type) && parentAllowsRemoval(node);
} Type guard
const isRemovableNodeType = (node, allowed) => allowed.has(node.type);
Try / catch
try {
mutations.push(removeNodeMutation(node));
} catch (e) {
if (e.message.startsWith('Cannot perform a remove mutation')) {
mutations.push(replaceWith(node, fallbackReplacement)); // removal unsupported: replace instead
} else throw e;
} Prevention
- Prefer replaceNodeMutation when removal semantics are unclear for a node type
- Maintain an allowlist set of removable types mirroring RemoveNode.js and guard your visitor
- Remove at the owning level (declarator, property, statement) rather than leaf values
When it happens
Trigger: removeNodeMutation(node) where node.type is e.g. Literal, CallExpression, BinaryExpression, Program, or any type-annotation node: anything without a dedicated removal case.
Common situations: Generic 'delete this node' codemod logic applied to arbitrary matched nodes; porting a codemod from jscodeshift, where remove() works on any Node, and assuming parity here.
Related errors
- Tried to remove ${node.type} from parent of type ${node.pare
- Tried to remove ${node.type} from parent of type ${node.pare
- Tried to remove ${node.type} from parent of type ${node.pare
- Could not find target in array of `${node.parent.type}.${key
- Attempted to mutate a `${node.type}.${key}` on a deleted nod
AI-assisted analysis of facebook/flow@d1341dac89 (2026-08-17).
Data as JSON: /api/errors/3add287daa726148.
Report an issue: GitHub.