Error Handling
Lanexio Parser splits errors into two worlds with a hard boundary:
- Document problems are data. Parsing and serializing never throw, no matter the input. Malformed content becomes error nodes inside a valid
LexTree. - Program problems are exceptions. Misconfigured deployments, invalid query patterns, malformed edits, and corrupt tree buffers throw typed errors with stable machine-readable codes — at construction/compile time, before any document is involved.
Design rule of thumb: try/catch belongs around startup and developer-input surfaces; request paths read hasError flags instead.
World 1 — error nodes (never-throw)
Section titled “World 1 — error nodes (never-throw)”Every parse function returns a valid tree. The LEX_NODE_HAS_ERROR flag bit marks error-carrying nodes, surfaced as node.hasError:
const tree = parseHtml(encoder.encode('<table><p>bad nesting'));// No try/catch needed — ever.Finding error nodes
Section titled “Finding error nodes”The flat AST makes a whole-tree error scan trivial:
import { LexNode } from '@lanexio/parser-core';
const errors: LexNode[] = [];for (let i = 0; i < tree.nodeCount; i++) { const node = new LexNode(tree, i); if (node.hasError) errors.push(node);}Or declaratively with LexQuery: LexQuery.compile('*[error]', resolver).
How each grammar reports document errors
Section titled “How each grammar reports document errors”The never-throw contract is universal; the shape of error reporting fits each format’s semantics:
| Grammar | Strategy | What you see |
|---|---|---|
| HTML | Recover and continue (per WHATWG spec recovery) | The tree is always a full document; ParseError leaf nodes are appended as an error catalog, each spanning the offending bytes. HtmlParseErrorCode names the spec violation classes. |
| Markdown | Recover and continue (CommonMark is forgiving by design) | Most “errors” are simply valid-by-spec fallbacks; genuinely unrepresentable input yields flagged error nodes. |
| YAML | Recover inline | YamlKind.Error nodes appear where recovery happened; surrounding structure is preserved. A catastrophic internal failure degrades to a flagged two-node Stream → Error tree. |
| CSS | Recover inline (per CSS Syntax error handling) | CssKind.Error nodes at the failed construct; rule-level recovery to the next ; or }. |
| JSON | All-or-nothing (RFC strictness) | Any violation yields a single-node tree whose root is JsonKind.Error, with range pointing at the failing byte offset. There is no partial JSON tree by design. |
// JSON's strict shape:const t = parseJson(encoder.encode('{"a": 1, nope'));if (t.root.kind === JsonKind.Error) { const [failedAt] = t.root.range; report(`invalid JSON at byte ${failedAt}`);}Serializers too
Section titled “Serializers too”serializeHtml and serializeMarkdown also never throw — both are fully iterative and survive 50,000-level-deep nesting (regression-tested). “Never-throw” covers the whole tree round trip, not just parsing.
World 2 — thrown errors with stable codes
Section titled “World 2 — thrown errors with stable codes”Every throwing surface uses an Error subclass carrying a code from a const object, so handling is switch-friendly and message changes never break callers:
| Class | Thrown by | Codes |
|---|---|---|
LanexioParserError | createParser (construction) | grammar_load_failed, protocol_mismatch |
LexQuerySyntaxError | LexQuery.compile | empty_pattern, unexpected_token, unknown_kind, unsupported_attribute — plus .offset into the pattern |
LexEditError | applyEdit (and core reparse’s input boundary) | range_out_of_bounds, range_inverted |
LexTreeValidationError | new LexTree(buffer, source) on untrusted buffers | buffer_too_small, invalid_magic, invalid_version, invalid_record_size, empty_tree, invalid_root_index, node_section_out_of_bounds, invalid_node_index, invalid_subtree_size, invalid_node_range |
WasmLoaderError | WASM grammar loaders | instantiation_failed, missing_exports, missing_toy_exports, protocol_mismatch |
import { LexQuery, LexQuerySyntaxError } from '@lanexio/parser-query';
try { query = LexQuery.compile(userPattern, resolver);} catch (err) { if (err instanceof LexQuerySyntaxError) { showSquiggle(userPattern, err.offset, err.code); // developer-facing input → loud return; } throw err;}Why these throw while parsing doesn’t: each one validates program input (a grammar pack you deployed, a pattern you wrote, offsets you computed, a buffer you transported). Failing fast there is a feature; the never-throw contract is reserved for document bytes, which are never your code’s fault.
Defense in depth
Section titled “Defense in depth”Beneath the contracts sit structural guarantees verified by fuzzing: iterative state machines (no stack overflows from deep nesting), forward-progress guards in loop-driven parsers (a dispatch bug degrades to an error node + one-byte advance, never a hang), and a belt-and-suspenders catch at every public entry point that would convert even an unexpected internal exception into an error tree. See Stability Guarantees for the full posture and fuzz numbers.