006. Syntax Error Reporting and Error Recovery
- Status: Implemented
- Author: @ornew
- Date: 2026-10-07
Summary
This record describes the design of syntax error reporting (positions and expectations, #error) and of the #recover attribute, which recovers from an error and continues parsing.
For the specification, see spec/attributes.md.
Error reporting
In PEG, failures are absorbed by backtracking, so "where the parse failed" is not unique. Following common practice, the parser reports the farthest failure position and the set of terminals expected at that position.
- Failures inside a lookahead are not reported, because
xfailing inside!xis the expected outcome. - Failures inside a Pratt
skipare not reported either, because they would be noise (such as whitespace). - When a memoized result is used outside a lookahead but was computed inside one, it is recomputed, because no expectations were recorded for it.
#error
A list of expectations exposes the grammar's internal structure and can be hard to read. #error(message=...) records the expectations inside its expression separately and, if the expression fails, replaces them with the message.
The reported position is the farthest position reached inside the expression, not the start of the expression. When name in "let" name is partially read, this points to a more accurate position.
Error recovery
Approaches considered
| Approach | Overview | Decision |
|---|---|---|
| Labeled failures with recovery rules | Attach labels to failures and define a recovery rule per label outside the grammar | Expressive, but the mapping between labels and recovery rules must be maintained separately |
| Synchronization tokens | On failure, skip to a specific token | Simple, but does not fit PEGO, which has no tokens (no scanner) |
#recover(skip=expr) on an expression |
On failure, skip the input described by a parser expression | Adopted. Where to recover and how to skip are written in the same place. |
Recovered errors and backtracking
Recovery turns a failure into a success, so it can also happen on a path that is later backtracked. Errors recovered on such a path must not be reported.
Recovered errors are therefore kept as an undoable record, like captures, and are undone when the parser switches alternatives (mark/reset).
The result of a memoized rule also stores the errors recovered during that call, and they are recorded again when the memo entry is used. For Pratt operator candidates, only the errors of the selected candidate are kept.
Preventing infinite loops
If skip consumes no input, no recovery takes place. This prevents a #recover inside a repetition from producing empty Error nodes forever at the end of the input.
When the whole parse fails
If the input as a whole does not match despite recovery, the recovered errors belong to a path that was ultimately not taken. In that case only the last syntax error is returned.