facebook/flow · error · Error

SimpleTransform.transformProgram: Expected program node.

Error message

SimpleTransform.transformProgram: Expected program node.

What it means

SimpleTransform.transformProgram is the typed wrapper around SimpleTransform.transform: it requires the transform to still yield a node whose type is 'Program'. The error fires when the result is null (root was removed) or a node of another type (root was replaced). It exists because everything downstream of transformProgram (printers, codemod pipelines) assumes a Program root.

Source

Thrown at packages/flow-parser/oxidized-src/transform/SimpleTransform.js:147

  /**
   * Transform the given AST tree.
   * @param node The root node to traverse.
   * @param options The option object.
   */
  static transform(node: ESNode, options: TransformOptions): ESNode | null {
    return new SimpleTransform().transform(node, options);
  }

  static transformProgram(
    program: Program,
    options: TransformOptions,
  ): Program {
    const result = SimpleTransform.transform(program, options);
    if (result?.type === 'Program') {
      return result;
    }
    throw new Error('SimpleTransform.transformProgram: Expected program node.');
  }

  /**
   * Return a new AST node with the given properties overrided if needed.
   *
   * This function takes care to only create new nodes when needed. Referential equality of nodes
   * is important as its used to know if a node should be re-traversed.
   *
   * @param node The base AST node.
   * @param overrideProps New properties to apply to the node.
   * @return Either the orginal node if the properties matched the existing node or a new node with
   *         the new properties.
   */
  static nodeWith<T extends ESNode>(
    node: T,
    overrideProps: Partial<T>,
    visitorKeys?: VisitorKeysType,
  ): T {

View on GitHub (pinned to d1341dac89)

Solutions

  1. Never remove or replace the root Program in a visitor; mutate its body instead
  2. Ensure the AST you pass in is an ESTree Program from flow-parser, not a File wrapper from another parser
  3. If you only need statement-level changes, return {...program, body: filteredBody} from the Program case

Example fix

// before
visitor.transform = (node) => {
  if (node.type === 'Program') return null; // removal -> throws
}

// after
visitor.transform = (node) => {
  if (node.type === 'Program') {
    return {...node, body: node.body.filter(notStripped)};
  }
}
Defensive patterns

Strategy: validation

Validate before calling

function assertProgramRoot(ast) {
  if (ast == null || ast.type !== 'Program') {
    throw new Error('Expected Program root, got ' + (ast && ast.type));
  }
}
assertProgramRoot(ast);
const out = SimpleTransform.transformProgram(ast, options);

Type guard

const isProgram = (n) => n != null && n.type === 'Program';

Try / catch

try {
  out = SimpleTransform.transformProgram(program, options);
} catch (e) {
  if (e.message.includes('Expected program node')) {
    // the visitor removed/replaced the root: fix the visitor, not the call site
    throw new Error('Transform dropped the Program root; check the Program visitor case');
  }
  throw e;
}

Prevention

When it happens

Trigger: A visitor returns null for the Program node (removal), or returns a node of a different type (e.g. an Expression or a custom node) as the new root; also when the input AST's root is not a Program in the first place and survives the transform unchanged.

Common situations: A visitor meant to strip some statements accidentally removes or replaces the root; reusing a visitor written for expression-level transforms on a whole program; passing a Babel-style File AST (root type 'File') instead of an ESTree Program.

Related errors


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