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.
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.
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
- You are parsing an established format (JSON, CSV, YAML, SQL dialects, XML): a purpose-built published parser is a dependency away and skips weeks of grammar work
- Your grammar is left-recursive or ambiguous: LL(k) forbids left recursion outright, so classic expression grammars must be rewritten with repetition, and grammars beyond k lookahead need the third-party ALL(*) plugin from the Langium team
- You want compact grammar-file syntax: a Chevrotain grammar is verbose JavaScript (CONSUME, SUBRULE, OR everywhere), easily several times the line count of an equivalent PEG file for peggy or nearley
- Bus factor worries you: this is a community project with a small maintainer circle and a mostly-maintenance cadence, not a corporate-backed toolchain
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 partsWith 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
| Package | Registry | Pick it when |
|---|---|---|
| peggy | npm | You prefer writing a compact PEG grammar file and generating the parser (successor of PEG.js) |
| nearley | npm | Your grammar is ambiguous or left-recursive; its Earley engine accepts grammars LL(k) cannot |
| ohm-js | npm | You want readable standalone grammar definitions with a semantics layer for quick DSL experiments |
| antlr4 | npm | You want the ANTLR ecosystem's existing grammars and multi-language targets over a JS-native toolkit |