Skip to content

Error Handling

Lanexio Parser splits errors into two worlds with a hard boundary:

  1. Document problems are data. Parsing and serializing never throw, no matter the input. Malformed content becomes error nodes inside a valid LexTree.
  2. 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.

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.

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).

The never-throw contract is universal; the shape of error reporting fits each format’s semantics:

GrammarStrategyWhat you see
HTMLRecover 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.
MarkdownRecover and continue (CommonMark is forgiving by design)Most “errors” are simply valid-by-spec fallbacks; genuinely unrepresentable input yields flagged error nodes.
YAMLRecover inlineYamlKind.Error nodes appear where recovery happened; surrounding structure is preserved. A catastrophic internal failure degrades to a flagged two-node Stream → Error tree.
CSSRecover inline (per CSS Syntax error handling)CssKind.Error nodes at the failed construct; rule-level recovery to the next ; or }.
JSONAll-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}`);
}

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:

ClassThrown byCodes
LanexioParserErrorcreateParser (construction)grammar_load_failed, protocol_mismatch
LexQuerySyntaxErrorLexQuery.compileempty_pattern, unexpected_token, unknown_kind, unsupported_attribute — plus .offset into the pattern
LexEditErrorapplyEdit (and core reparse’s input boundary)range_out_of_bounds, range_inverted
LexTreeValidationErrornew LexTree(buffer, source) on untrusted buffersbuffer_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
WasmLoaderErrorWASM grammar loadersinstantiation_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.

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.