# Handle parse errors

Dispatch on the three parse-result states, read diagnostics and spans, and choose strict or lenient parsing.

https://texform.dev/docs/handle-parse-errors

A parse result has exactly three honest states, and robust code dispatches on all three rather than assuming a document always exists. The document is `None` when the parser had nothing to recover; it is present but `has_errors()` when recovery preserved the unparseable parts as read-only `Error` nodes; and it is present and clean otherwise. Each state carries different information: `None` is explained entirely by the diagnostics list, an error tree exposes the failing spans through its `Error` nodes, and a clean tree is ready to serialize or transform.

The function below handles every state in one pass. For the no-tree case it reads the first diagnostic's message and span; for the partial case it reads the span off each `Error` node; for the clean case it serializes the tree back to LaTeX.

**Python**

```python
import texform

def classify(parser, src):
    result = parser.parse(src)
    doc = result["document"]
    if doc is None:
        # Hard failure: no tree. Diagnostics describe what went wrong.
        first = result["diagnostics"][0]
        return f"NO TREE   {src!r}: {first['message']} at {first['span']}"
    if doc.has_errors():
        # Partial, read-only tree. Error nodes carry the unparseable spans.
        spans = [n.span() for n in doc.errors()]
        return f"PARTIAL   {src!r}: {len(doc.errors())} error node(s) at {spans}"
    # Clean, editable tree.
    return f"CLEAN     {src!r}: {doc.to_latex()}"

parser = texform.Parser()
for src in [r"\frac{x}{y}", r"x^", r"\frac{a}{b", r""]:
    print(classify(parser, src))
```

**JavaScript**

```js
import { Parser } from 'texform';

function classify(parser, src) {
  const result = parser.parse(src);
  const doc = result.document;
  if (doc === null) {
    // Hard failure: no tree. Diagnostics describe what went wrong.
    const first = result.diagnostics[0];
    return `NO TREE   ${JSON.stringify(src)}: ${first.message} at ${JSON.stringify(first.span)}`;
  }
  if (doc.hasErrors()) {
    // Partial, read-only tree. Error nodes carry the unparseable spans.
    const spans = doc.errors().map((n) => n.span());
    return `PARTIAL   ${JSON.stringify(src)}: ${doc.errors().length} error node(s) at ${JSON.stringify(spans)}`;
  }
  // Clean, editable tree.
  return `CLEAN     ${JSON.stringify(src)}: ${doc.toLatex()}`;
}

const parser = new Parser();
for (const src of ['\\frac{x}{y}', 'x^', '\\frac{a}{b', '']) {
  console.log(classify(parser, src));
}
```

Each input lands in a distinct branch, and empty input is a clean tree rather than a failure:

```text
CLEAN     '\\frac{x}{y}': \frac { x } { y }
PARTIAL   'x^': 1 error node(s) at [{'start': 0, 'end': 2}]
NO TREE   '\\frac{a}{b': unclosed brace argument at {'start': 8, 'end': 9}
CLEAN     '':
```

A diagnostic is a record with a `message`, a `kind` (a stable string such as `unknown-command`, or absent for some structural errors), and a `span` locating it in the source. The `span` on an `Error` node and the `span` on a diagnostic both point at the trouble, so either can drive a highlight in your own tooling.

## Spans are UTF-8 byte offsets [#spans-are-utf-8-byte-offsets]

A span is a half-open range of **UTF-8 byte** offsets — not Python code-point indices, and not JavaScript UTF-16 indices. On pure ASCII the distinction never shows up, which is exactly why it bites later. Any non-ASCII character earlier in the formula shifts every following span:

```python
import texform

src = "\\alpha β x^"
result = texform.Parser().parse(src)

print("characters:", len(src), "bytes:", len(src.encode()))
print("error span:", result["document"].errors()[0].span())
print("slice by byte:", src.encode()[10:12])
```

```text
characters: 11 bytes: 12
error span: {'start': 10, 'end': 12}
slice by byte: b'x^'
```

The `β` occupies two bytes, so the span that covers `x^` reads `10..12` even though those characters start at index 9 of the Python string. Index into the encoded bytes and decode the slice, rather than indexing the string directly.

## Choosing strictness [#choosing-strictness]

Two independent knobs decide which states you ever see. Both default to lenient — `reject_unknown` and `abort_on_error` are each `False` unless you say otherwise:

| Setting                            | Result on bad input                                                                                                                                                                | When to use                                                                          |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `abort_on_error = False` (default) | Syntax errors are recovered as `Error` nodes, and you triage the result with `has_errors()`. Recovery is best-effort: input the parser cannot recover at all still yields no tree. | Noisy input you want to inspect and partially salvage (OCR/MER output, exploration). |
| `abort_on_error = True`            | The first syntax error per item stops the parse and produces no recovery `Error` nodes.                                                                                            | Input you expect to be clean and want to reject loudly at the first defect.          |
| `reject_unknown = False` (default) | Unknown commands survive as plain `known: false` nodes.                                                                                                                            | Corpora full of project macros you would rather keep than reject.                    |
| `reject_unknown = True`            | Unknown commands become diagnostics rather than surviving silently.                                                                                                                | Catching invented or misspelled commands.                                            |

The knobs are orthogonal, and neither one is equivalent to the tree's `has_errors()` signal. `\frac{a}{b` in the run above returned no tree under the default lenient settings, which is why dispatching on all three states matters more than picking the right strictness.

For the full four-quadrant behavior of these two knobs and the parse-state model behind them, see [Parsing](https://texform.dev/docs/parsing); for sorting a real recognition batch into repair and discard buckets with these diagnostics, see [Clean OCR/MER output](https://texform.dev/docs/clean-ocr-mer-output).

## Next steps [#next-steps]

- [Clean OCR/MER output](https://texform.dev/docs/clean-ocr-mer-output) — Turn these three states into repair and discard buckets over a real batch.

- [Parsing](https://texform.dev/docs/parsing) — The knowledge base, the strictness quadrants, and the parse-state model.

- [Edit a document](https://texform.dev/docs/edit-a-document) — What you can do with a clean tree once it survives triage.

- [Troubleshooting](https://texform.dev/docs/troubleshooting) — Errors raised outside the parser: config, foreign documents, and edits.

