mrkeyoor.com_
Wed 05 Aug 19:57 UTC
npmAI / MLupdated 05 Aug 2026

chevrotain

Chevrotain is a parser building toolkit for JavaScript: you write a lexer and an LL(k) grammar as plain JS or TS classes, with no separate grammar file and no code generation step, and it hands you a concrete syntax tree plus visitors to walk it. Its fault-tolerant error recovery is why it powers language tooling like Langium, Prettier-Java, and HyperFormula's formula engine, where a parser must survive half-typed input inside an editor. Debugging is ordinary JS debugging, since the grammar is just code.

Verdict

For serious language tooling in JavaScript, Chevrotain is the strongest pure-JS option: fast, debuggable, and built for fault tolerance, with Langium and Prettier-Java as proof. It is overkill for parsing known formats and demands real learning; budget days to get comfortable, not hours.

API stability4/5The core surface (createToken, Lexer, CstParser, visitors) has been steady for years; the many major version bumps mostly track dropped legacy runtimes and internal package splits rather than API rewrites
Docs4/5A step-by-step tutorial, an online playground, sample grammars for real languages, and an FAQ that answers the vs-generators question honestly; API reference depth is thinner than the guides
Maintenance3/5Pushed days before this review and releases still land, but the pace is maintenance-mode with a small maintainer circle; the low count of 29 open issues and PRs reflects a settled, not booming, project
Ecosystem3/5Impressive named adopters (Langium, Prettier-Java, HyperFormula, JHipster JDL) and a third-party ALL(*) lookahead plugin, but the plugin and tutorial ecosystem is small compared to ANTLR's world

Use it if

  • You are building a real language or DSL (query language, formula syntax, config format of your own design) and need control over tokens, lookahead, and error messages
  • You need editor-grade fault tolerance: error recovery is built in, so the parser produces a usable CST from broken input instead of dying at the first mistake
  • You want to debug the grammar with normal breakpoints and stack traces, which generated-parser tools cannot offer because their runtime is generated code
  • Parsing throughput matters: the project publishes benchmarks and its hand-tuned LL(k) engine is among the fastest pure-JS parsing options
Skip it if

Setup reality

npm install chevrotain is clean: its only dependencies are its own @chevrotain scoped packages, it ships ESM and CJS plus browser bundles on CDNs, and TypeScript types are included. The setup cost is conceptual, not mechanical: you must learn the token-before-parser workflow, remember that token array order decides lexing priority, call performSelfAnalysis at the end of every parser constructor, and reuse a single parser instance because construction runs expensive grammar analysis. Getting CST types for TypeScript means running its cst-dts-gen tooling as an extra step.

Patterns

Define tokens and build a lexerdefine-tokens

import { createToken, Lexer } from "chevrotain";

const Identifier = createToken({ name: "Identifier", pattern: /[a-zA-Z]\w*/ });
const Integer = createToken({ name: "Integer", pattern: /\d+/ });
const Comma = createToken({ name: "Comma", pattern: /,/ });

const allTokens = [Integer, Comma, Identifier];
const lexer = new Lexer(allTokens);

Array order is lexing priority: earlier tokens win when two patterns match at the same position. Put keywords and longer or more specific patterns before general ones like Identifier.

Skip whitespace and commentsskip-whitespace

const WhiteSpace = createToken({
  name: "WhiteSpace",
  pattern: /\s+/,
  group: Lexer.SKIPPED,
});
const LineComment = createToken({
  name: "LineComment",
  pattern: /\/\/[^\n]*/,
  group: Lexer.SKIPPED,
});

SKIPPED tokens are matched but never reach the parser, so grammar rules stay clean. Use a named group instead of Lexer.SKIPPED if you need to keep comments for formatting tools.

Keep keywords from eating identifierskeyword-vs-identifier

const Identifier = createToken({ name: "Identifier", pattern: /[a-zA-Z]\w*/ });
const Select = createToken({
  name: "Select",
  pattern: /SELECT/,
  longer_alt: Identifier,
});
// order: keywords first, Identifier last
const allTokens = [WhiteSpace, Select, Identifier];

Without longer_alt, the input SELECTED lexes as the keyword SELECT plus ED. longer_alt tells the lexer to prefer the longer Identifier match when the keyword is only a prefix.

Write a grammar as a CstParser classcst-parser-basic

import { CstParser } from "chevrotain";

class SelectParser extends CstParser {
  constructor() {
    super(allTokens);
    const $ = this;

    $.RULE("selectStatement", () => {
      $.CONSUME(Select);
      $.SUBRULE($.columns);
      $.CONSUME(From);
      $.CONSUME(Identifier);
    });

    $.RULE("columns", () => {
      $.AT_LEAST_ONE_SEP({ SEP: Comma, DEF: () => $.CONSUME(Identifier) });
    });

    this.performSelfAnalysis();
  }
}

performSelfAnalysis at the end of the constructor is mandatory; forgetting it is the classic first bug. Grammar problems (ambiguities, left recursion) are detected there and thrown as descriptive errors.

Choose between alternatives with ORalternatives-or

$.RULE("value", () => {
  $.OR([
    { ALT: () => $.CONSUME(StringLiteral) },
    { ALT: () => $.CONSUME(Integer) },
    { ALT: () => $.SUBRULE($.object) },
    { ALT: () => $.SUBRULE($.array) },
  ]);
});

Alternatives must be distinguishable within k tokens of lookahead or self-analysis reports an ambiguity. GATE predicates can disambiguate manually when token types alone cannot.

Repetition with MANY and OPTIONrepetition

$.RULE("statementList", () => {
  $.MANY(() => $.SUBRULE($.statement));
});

$.RULE("columnRef", () => {
  $.CONSUME(Identifier);
  $.OPTION(() => {
    $.CONSUME(Dot);
    $.CONSUME2(Identifier);
  });
});

Consuming the same token type twice in one rule needs numbered suffixes (CONSUME2, CONSUME3); duplicate unnumbered calls throw at self-analysis time. The numbers also become distinct CST keys.

Lex, parse, and check errorsrun-parser

const parser = new SelectParser(); // create ONCE, reuse

function parse(text) {
  const lexResult = lexer.tokenize(text);
  parser.input = lexResult.tokens; // also resets parser state
  const cst = parser.selectStatement();
  if (lexResult.errors.length || parser.errors.length) {
    throw new Error(JSON.stringify([...lexResult.errors, ...parser.errors]));
  }
  return cst;
}

Parser construction runs grammar analysis and is expensive; instantiate once and reuse by assigning parser.input per parse. Check both lexer errors and parser errors, they are separate lists.

Walk the CST with a visitorcst-visitor

const BaseVisitor = parser.getBaseCstVisitorConstructor();

class ToAstVisitor extends BaseVisitor {
  constructor() {
    super();
    this.validateVisitor();
  }
  selectStatement(ctx) {
    return {
      type: "select",
      columns: this.visit(ctx.columns),
      table: ctx.Identifier[0].image,
    };
  }
  columns(ctx) {
    return ctx.Identifier.map((t) => t.image);
  }
}

CST node children are arrays keyed by rule and token names, so it is ctx.Identifier[0], not ctx.Identifier. validateVisitor throws early if a grammar rule lacks a visitor method.

Fault-tolerant parsing for editor toolingerror-recovery

class TolerantParser extends CstParser {
  constructor() {
    super(allTokens, { recoveryEnabled: true });
    // ...rules...
    this.performSelfAnalysis();
  }
}
// after parsing broken input:
// parser.errors describes problems, the CST still covers the valid parts

With recoveryEnabled the parser re-syncs after errors instead of stopping, which is what language servers need. Recovered CSTs can contain missing nodes, so visitors must handle absent keys.

Group tokens with categoriestoken-categories

const AdditionOperator = createToken({ name: "AdditionOperator", pattern: Lexer.NA });
const Plus = createToken({ name: "Plus", pattern: /\+/, categories: AdditionOperator });
const Minus = createToken({ name: "Minus", pattern: /-/, categories: AdditionOperator });

// in a rule: matches Plus or Minus
$.CONSUME(AdditionOperator);

Categories with pattern Lexer.NA are abstract: never lexed directly, but consumable in rules. This keeps expression grammars short and makes adding operators a one-line change.

Precedence without left recursionexpression-precedence

$.RULE("additionExpression", () => {
  $.SUBRULE($.multiplicationExpression);
  $.MANY(() => {
    $.CONSUME(AdditionOperator);
    $.SUBRULE2($.multiplicationExpression);
  });
});

$.RULE("multiplicationExpression", () => {
  $.SUBRULE($.atomicExpression);
  $.MANY(() => {
    $.CONSUME(MultiplicationOperator);
    $.SUBRULE2($.atomicExpression);
  });
});

LL(k) bans expr := expr + expr, so precedence is encoded as one rule per level, lowest first, each deferring to the next. The flat CST it produces means the visitor rebuilds associativity.

Alternatives

PackageRegistryPick it when
peggynpmYou prefer writing a compact PEG grammar file and generating the parser (successor of PEG.js)
nearleynpmYour grammar is ambiguous or left-recursive; its Earley engine accepts grammars LL(k) cannot
ohm-jsnpmYou want readable standalone grammar definitions with a semantics layer for quick DSL experiments
antlr4npmYou want the ANTLR ecosystem's existing grammars and multi-language targets over a JS-native toolkit