chevrotain review
Chevrotain 13.2.0 is a JavaScript and TypeScript toolkit for building an LL(k) lexer and parser in application code. You describe tokens, grammar rules, recovery, and CST visitors through its API; it does not read a grammar file or generate a parser. The 13.2 release changes lexer internals: switch-based strategy dispatch and faster scanning of skipped ASCII character classes improved the project's CSS and JSON lexer benchmarks. Version 13 also uses -1 for missing token and CST positions, so location checks written for NaN need updating.
Chevrotain 13.2.0 installed in 3.8 seconds and bundled to 30.6 KB gzipped in our sandbox, making it a defensible browser parser toolkit when you need recovery and CST tooling. Skip it for standard data formats, pre-Node-22 deployments, or grammars that fight LL(k).
We installed it
| Install | ✓ · 3.8s | 6 packages on disk · 3 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 30.6 KB | gzipped (111.1 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does chevrotain install cleanly?
Yes. In a fresh container with an empty cache, npm install chevrotain finished in 4 seconds, leaving 6 packages and 3 MB on disk. npm audit reported no known vulnerabilities.
How much does chevrotain add to a browser bundle?
30.6 KB gzipped (111.1 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does chevrotain work with both ESM and CommonJS?
Yes. Both import 'chevrotain' and require('chevrotain') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does chevrotain include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
chevrotain or ohm-js: which should you use?
Pick ohm-js when ohm-js 17.5.0 fits when a declarative grammar and separate semantic operations are easier for the team to review. Chevrotain 13.2.0 installed in 3.8 seconds and bundled to 30.6 KB gzipped in our sandbox, making it a defensible browser parser toolkit when you need recovery and CST tooling.
When should you not use chevrotain?
Your production runtime is below Node 22. Package 13.2.0 declares Node >=22.0.0, even though earlier Chevrotain majors supported older releases.
Use it if
- Chevrotain 13.2.0 fits a DSL, editor, formatter, or language server that needs token positions plus a concrete syntax tree.
- Grammar construction should reject left recursion, duplicate DSL occurrences, unresolved rules, and ambiguous choices before requests arrive.
- Incomplete editor input must produce errors and a partly usable CST through configurable recovery.
- Your team wants lexer modes, token categories, syntactic content assist, and visitors in one TypeScript-facing package.
- Your production runtime is below Node 22. Package 13.2.0 declares Node >=22.0.0, even though earlier Chevrotain majors supported older releases.
- The grammar is left recursive or regularly needs unbounded lookahead. Core Chevrotain accepts LL(k); the project points LL(*) users to a separate plugin.
- You want a standalone grammar file and generated parser source. Chevrotain expresses rules as numbered method calls inside JavaScript or TypeScript.
- You only parse JSON, CSV, YAML, or another settled format. A format-specific parser avoids owning token precedence, recovery policy, and CST conversion.
- A 30.6 KB gzipped toolkit is too much for the browser path. That measured bundle excludes the grammar, application visitor, and surrounding editor code.
- Nobody on the team is comfortable maintaining LL grammar structure. Repeated CONSUME and SUBRULE calls need occurrence suffixes, and precedence cannot use left recursion.
Setup reality
Our fresh-sandbox install of chevrotain 13.2.0 took 3.8 seconds in Node 22 Bookworm, placed 6 packages on disk, and used 3 MB. npm audit reported 0 known vulnerabilities. The package itself has 5 direct dependencies, no peer dependencies, and 1672 KB unpacked. It ships TypeScript declarations. Both require() and ESM import worked in our Node 22.23.2 checks despite the package declaring ESM with an exports map.
We measured 111.1 KB minified and 30.6 KB gzipped for a browser import of the whole package in esbuild. Define specific tokens before a catch-all Identifier and use longer_alt when a keyword may prefix an identifier. Whitespace does not disappear automatically; assign it to Lexer.SKIPPED, or keep it in a named group when comments and formatting matter.
Every parser class must register its RULE calls and then run performSelfAnalysis(). That startup pass detects grammar defects and builds lookahead functions, so construct one parser and reuse it by replacing parser.input. Lexing and parsing return different error arrays. Check both before accepting a result, including when recoveryEnabled is true.
In version 13, unavailable locations are -1 rather than NaN. Any Number.isNaN location test will silently miss the new sentinel. Recovered CST nodes can also omit children that a visitor normally receives, so editor-facing visitors need guards and malformed-input tests. The 13.2 lexer changes are internal; your grammar API remains the same, but the Node 22 floor is a deployment constraint.
Patterns
Tokenize identifiers and commas tokenize-input
import { createToken, Lexer } from 'chevrotain';
const Space = createToken({ name: 'Space', pattern: /\s+/, group: Lexer.SKIPPED });
const Comma = createToken({ name: 'Comma', pattern: /,/ });
const Identifier = createToken({ name: 'Identifier', pattern: /[A-Za-z_]\w*/ });
const vocabulary = [Space, Comma, Identifier];
const lexer = new Lexer(vocabulary);Token order is priority order when patterns overlap. Inspect tokenize(...).errors before parsing its tokens.
Keep keyword prefixes inside identifiers match-keywords
const Identifier = createToken({ name: 'Identifier', pattern: /[A-Za-z_]\w*/ });
const Let = createToken({ name: 'Let', pattern: /let/, longer_alt: Identifier });
const vocabulary = [Space, Let, Identifier];longer_alt makes letter one Identifier instead of Let followed by ter. Keep the keyword before Identifier.
Build and analyze a list grammar define-parser
class ListParser extends CstParser {
constructor() {
super(vocabulary);
this.RULE('list', () => {
this.AT_LEAST_ONE_SEP({ SEP: Comma, DEF: () => this.CONSUME(Identifier) });
});
this.performSelfAnalysis();
}
}performSelfAnalysis() must follow every RULE registration. Construction throws when the grammar has structural conflicts.
Check errors from both stages reuse-parser
const parser = new ListParser();
function parse(text) {
const lex = lexer.tokenize(text);
parser.input = lex.tokens;
const cst = parser.list();
const errors = [...lex.errors, ...parser.errors];
if (errors.length) throw new Error(JSON.stringify(errors));
return cst;
}Assigning parser.input resets parse state for the next call. Lexer errors never appear in parser.errors.
Disambiguate repeated DSL calls number-occurrences
this.RULE('pair', () => {
this.CONSUME(Identifier);
this.CONSUME(Colon);
this.CONSUME2(Identifier);
});A second CONSUME of the same token in one rule needs the 2 suffix. Duplicate occurrence indexes fail analysis.
Turn CST tokens into an array build-cst-visitor
const Base = parser.getBaseCstVisitorConstructor();
class ListVisitor extends Base {
constructor() { super(); this.validateVisitor(); }
list(ctx) { return (ctx.Identifier ?? []).map((token) => token.image); }
}CST child properties are arrays. The fallback covers a recovered node whose Identifier child is missing.
Enable recovery for editor input recover-errors
class EditorParser extends CstParser {
constructor() {
super(vocabulary, { recoveryEnabled: true });
this.RULE('document', () => { /* grammar */ });
this.performSelfAnalysis();
}
}Recovery records parser errors and attempts resynchronization. It does not clear lexer errors or make input valid.
Model precedence without left recursion encode-precedence
this.RULE('sum', () => {
this.SUBRULE(this.product);
this.MANY(() => {
this.CONSUME(Plus);
this.SUBRULE2(this.product);
});
});Chevrotain rejects a rule that calls itself before consuming input. Give each precedence level its own rule.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ohm-js | npm | ohm-js 17.5.0 fits when a declarative grammar and separate semantic operations are easier for the team to review. |
| nearley | npm | Choose it when an ambiguous or non-LL grammar needs Earley parsing and multiple parse results are acceptable. |
| peggy | npm | Choose it when a PEG grammar and generated parser fit better than a separate lexer plus CST visitor. |
More utils guides
lru-cache · type-fest · ajv · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

