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

  1. Use replaceNodeMutation (or replace-with) to swap the node for what should remain, instead of removing it
  2. 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)
  3. 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

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


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