facebook/flow · error · Error

unable to resolve scope

Error message

unable to resolve scope

What it means

Thrown inside flowDefToTSDef.js while checking whether a name resolves to a React import (isReactImport). It walks up from scopeNode via node.parent calling scopeManager.acquire(node, true) until a scope is found; if the whole parent chain yields no scope it throws 'unable to resolve scope'. This happens when the AST node has no parent pointers or the scopeManager was built from a different AST than the node belongs to.

Source

Thrown at packages/flow-api-translator/src/flowDefToTSDef.js:320

      return globalScope.childScopes[0];
    }
    return globalScope;
  })();

  function isReactImport(scopeNode: FlowESTree.ESNode, name: string): boolean {
    let currentScope = (() => {
      let scope = null;
      let node: FlowESTree.ESNode = scopeNode;
      while (!scope && node) {
        scope = scopeManager.acquire(node, true);
        node = node.parent;
      }

      return scope;
    })();

    if (currentScope == null) {
      throw new Error('unable to resolve scope');
    }

    const variableDef = (() => {
      while (currentScope != null) {
        for (const variable of currentScope.variables) {
          if (variable.defs.length && variable.name === name) {
            return variable;
          }
        }
        currentScope = currentScope.upper;
      }
    })();

    // No variable found, it is not imported.
    // It could be a global though if isValidReactImportOrGlobal returns true.
    if (variableDef == null) {
      return false;
    }

View on GitHub (pinned to 5c86586199)

Solutions

  1. Use the public APIs (translateFlowDefToTSDef / translateFlowToTSDef from flow-api-translator) which build the AST and scopeManager together.
  2. If calling internals, re-parse the code and re-run scope analysis so every node reachable from scopeNode has parent pointers and an acquirable scope.
  3. Verify the scopeManager and AST come from the same source string before invoking translation.
  4. Wrap the call in try/catch and surface the offending node location for triage.

Example fix

// before
const {flowDefToTSDef} = require('flow-api-translator/src/flowDefToFlowDef.js');
const ast = parse(code); // no parents, no scope analysis
flowDefToTSDef(ast, code, someOtherScopeManager, opts);

// after
import {translateFlowDefToTSDef} from 'flow-api-translator';
const result = await translateFlowDefToTSDef({code, recoverFromErrors: true});
Defensive patterns

Strategy: validation

Validate before calling

// Verify the node can reach a scope before translating
function nodeResolvesToScope(scopeManager, node) {
  let cur = node;
  while (cur) {
    if (scopeManager.acquire(cur, true) != null) return true;
    cur = cur.parent;
  }
  return false;
}

Type guard

function hasParentChain(node, upTo = 100) {
  let cur = node, i = 0;
  while (cur && i++ < upTo) cur = cur.parent;
  return cur != null;
}

Try / catch

try {
  const out = await translateFlowDefToTSDef({code, recoverFromErrors: true});
} catch (e) {
  if (e.message === 'unable to resolve scope') {
    throw new Error('AST/scopeManager mismatch: re-parse and re-analyze the same source before translating');
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling internal flow-api-translator functions (flowDefToTSDef, isReactImport path) with a node whose .parent chain is broken: a detached/hand-built node, a Program parsed by flow-parser without scope analysis, or a scopeManager produced from a different parse of the code. Reached during JSX translation in translateFlowDefToTSDef/translateFlowToTSDef.

Common situations: Using the translator's internal modules directly instead of the public translateFlowDefToTSDef API (which parses and analyzes coherently); pre-transforming the AST with a custom pass that drops parent pointers; passing a scopeManager from a stale copy of the code.

Related errors


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