facebook/flow · error · SyntaxError

Syntax error in selector

Error message

Syntax error in selector "${rawSelector}" at position ${err.location.start.offset}: ${err.message}

What it means

SyntaxError thrown by flow-transform's traverse when an esquery selector fails to parse. The raw selector (with a trailing `:exit` stripped) goes through `esquery.parse`; parse errors that carry a location are rethrown as SyntaxError with the offending selector, the character offset, and esquery's message. Thrown during listener registration, before traversal starts.

Solutions

  1. Validate every visitor key with `esquery.parse` at startup or in unit tests before wiring traverse.
  2. Remember `:exit` is the only supported suffix and it must be last (it is stripped before parsing).
  3. Check esquery's selector grammar (identifiers, `>`, ` `, `,`, attribute brackets, `:not`, `:matches`, `:nth-child(n)`).
  4. Log the raw selector on failure - the reported offset indexes into the pre-`:exit` string.

Example fix

// before
traverseWithContext(code, ast, scope, () => ({}), ctx => ({
  'FunctionDeclaration >': node => {}, // dangling combinator - SyntaxError
}));

// after
traverseWithContext(code, ast, scope, () => ({}), ctx => ({
  'FunctionDeclaration > Identifier': node => {},
}));
Defensive patterns

Strategy: validation

Validate before calling

import esquery from 'esquery';

function isValidSelector(rawSelector: string): boolean {
  try {
    esquery.parse(rawSelector.replace(/:exit$/u, ''));
    return true;
  } catch {
    return false;
  }
}

const bad = Object.keys(selectors).filter(s => !isValidSelector(s));
if (bad.length > 0) throw new Error(`invalid selectors: ${bad.join(', ')}`);

Try / catch

try {
  traverseWithContext(code, ast, scope, () => ({}), visitors);
} catch (e) {
  if (e instanceof SyntaxError && /selector/.test(e.message)) {
    // a visitor key is malformed - the message includes the offset into the selector
  }
  throw e;
}

Prevention

When it happens

Trigger: A visitor key that is not valid esquery: unbalanced brackets (`'CallExpression >'`), a bare `':exit'` (empty after stripping), invalid pseudos (`':nth-child()'` without an argument), stray characters, or dynamically built selector strings that are malformed.

Common situations: Programmatically composing selectors (template strings with missing parts); typos in `:exit` (`':exi'`); using CSS features esquery does not support; quotes lost when selectors pass through JSON configs.

Related errors


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

Appendix: source

Thrown at packages/flow-transform/src/traverse/NodeEventGenerator.js:235

  );
}

/**
 * Parses a raw selector string, and throws a useful error if parsing fails.
 * @param rawSelector A raw AST selector
 * @returns An object (from esquery) describing the matching behavior of this selector
 * @throws An error if the selector is invalid
 */
function tryParseSelector(rawSelector: string): Selector {
  try {
    return esquery.parse(rawSelector.replace(/:exit$/u, ''));
  } catch (err) {
    if (
      err.location &&
      err.location.start &&
      typeof err.location.start.offset === 'number'
    ) {
      throw new SyntaxError(
        `Syntax error in selector "${rawSelector}" at position ${err.location.start.offset}: ${err.message}`,
      );
    }
    throw err;
  }
}

const selectorCache = new Map<string, ParsedSelector>();

/**
 * Parses a raw selector string, and returns the parsed selector along with specificity and type information.
 * @param rawSelector A raw AST selector
 * @returns A selector descriptor
 */
function parseSelector(rawSelector: string): ParsedSelector {
  const cachedSelector = selectorCache.get(rawSelector);
  if (cachedSelector) {
    return cachedSelector;

View on GitHub (pinned to f88ac94bcf)