# Troubleshooting

Fix common TeXForm failures: WASM loading, ConfigError on config shapes, mismatched knowledge bases, incomplete trees, and package conflicts.

https://texform.dev/docs/troubleshooting

When TeXForm refuses to do something, it almost always says so through a structured exception rather than a silent wrong answer, so the error class and its message are the fastest route to the cause. This page lists the failures people actually hit, each as a symptom you can recognize and the check that resolves it. The Python messages below are real interpreter output; the JavaScript counterparts throw the `Texform*` class named in each section.

## If importing `texform` fails in a bundler, check top-level await and `.wasm` assets [#if-importing-texform-fails-in-a-bundler-check-top-level-await-and-wasm-assets]

The npm package is WebAssembly, and it ships two entry points for loading it. The default `texform` import resolves to the Node entry under Node.js and to the bundler entry in browser-oriented bundlers; `texform/node` and `texform/bundler` force one explicitly.

The bundler entry initializes the WebAssembly module at module load time, so it needs a bundler that supports top-level `await` and can serve `.wasm` as an asset — Vite or webpack 5, for example. A build that fails on top-level `await`, or a runtime 404 for the `.wasm` file, means the bundler is not configured for one of those two things. A test runner or server-side script that picked the bundler entry by mistake is the other common shape of this failure, and the fix is to import the Node entry directly.

```js
// In Node.js — skips the bundler entry and its top-level await entirely.
import { TransformEngine } from 'texform/node';

// In a browser bundle — requires top-level await and .wasm asset support.
import { TransformEngine } from 'texform/bundler';
```

## If you get `ConfigError` or `TexformConfigError`, check the shape of the config you passed [#if-you-get-configerror-or-texformconfigerror-check-the-shape-of-the-config-you-passed]

Configuration is validated strictly, and the message carries the field path that failed. Three shapes account for nearly every case.

**An unknown or misspelled key.** Per-call options are an overlay, and the validator lists the keys it would have accepted:

**Python**

```python
import texform

engine = texform.TransformEngine(profile="corpus")

try:
    engine.normalize(r"a \over b", rewrite={"enable": False})
except texform.ConfigError as exc:
    print("ConfigError:", exc)

# The accepted spelling:
print(engine.normalize(r"a \over b", rewrite={"enabled": False}))
```

```text
ConfigError: invalid normalize overrides: rewrite.enable: unknown field `enable`, expected `enabled` or `max_iterations`
a \over b
```

**JavaScript**

```js
import { TransformEngine, TexformConfigError } from 'texform';

const engine = new TransformEngine({ profile: 'corpus' });

try {
  engine.normalize('a \\over b', { rewrite: { enable: false } });
} catch (exc) {
  if (exc instanceof TexformConfigError) console.log(exc.message);
}

// The accepted spelling:
console.log(engine.normalize('a \\over b', { rewrite: { enabled: false } }));
```

**A snake_case key in JavaScript.** The JavaScript overlay is camelCase throughout; `reject_unknown` is rejected where `rejectUnknown` is accepted. Enum *values* stay snake_case (`'sub_first'`), which is the one place the two conventions meet in a single object. Python is the mirror image: keys are snake_case there.

**A dict used where a complete config object belongs.** In Python the second positional argument is a complete `TransformConfig` (or `ParseConfig` on `parse`), not a mapping. Pass adjustments as keyword overlays instead, and note that nested overlay values must be plain dicts rather than phase-config instances:

```python
import texform

engine = texform.TransformEngine(profile="corpus")

try:
    engine.normalize(r"a \over b", {"rewrite": {"enabled": False}})
except texform.ConfigError as exc:
    print("ConfigError:", exc)

try:
    engine.normalize(r"a \over b", rewrite=texform.RewriteConfig(enabled=False, max_iterations=32))
except texform.ConfigError as exc:
    print("ConfigError:", exc)
```

```text
ConfigError: config must be a TransformConfig; pass overrides as keyword arguments (use **overrides for a dict)
ConfigError: invalid normalize overrides: rewrite: unsupported value of type `RewriteConfig`; pass a dict
```

If you do want a complete object, build one with `TransformConfig.corpus()` (or the matching profile classmethod) and adjust its fields, rather than constructing `TransformConfig` from scratch — every phase is required there.

## If `transform` rejects a document, check which knowledge base it was built on [#if-transform-rejects-a-document-check-which-knowledge-base-it-was-built-on]

In-place transformation requires a document built on the engine's own knowledge-base instance. Knowledge bases compare by identity, so a document from a parser with a separately constructed `KnowledgeBase` is foreign to the engine — even when both load the same packages — and is rejected instead of being silently normalized against different knowledge.

```python
import texform

engine = texform.TransformEngine(profile="corpus")

other = texform.Parser(knowledge_base=texform.KnowledgeBase())
foreign = other.parse(r"a \over b")["document"]

try:
    engine.transform(foreign)
except texform.TransformError as exc:
    print("TransformError:", exc)

# Parse through the engine (or share one KnowledgeBase), and the same flow works.
own = engine.parse(r"a \over b")["document"]
engine.transform(own)
print(own.to_latex())
```

```text
TransformError: document and transform engine use different knowledge-base instances; build one KnowledgeBase and share it with the parser, engine, and documents
\frac { a } { b }
```

The one-shot `normalize` never hits this, because it parses and transforms in a single call. Reach for `engine.parse` whenever you need the tree in between.

## If normalization rejects a formula, check whether the tree is complete [#if-normalization-rejects-a-formula-check-whether-the-tree-is-complete]

Normalization is gated on a complete tree: a formula with holes in it has no meaningful canonical form, so the engine declines rather than guessing. The two entry points report this differently, and it is worth handling both.

```python
import texform

engine = texform.TransformEngine(profile="corpus")

try:
    engine.normalize(r"x^")
except texform.ParseError as exc:
    print("ParseError:", exc)
    print("  diagnostics:", exc.diagnostics)
    print("  partial document recovered:", exc.document is not None)

partial = engine.parse(r"x^")["document"]
print("has_errors:", partial.has_errors())
try:
    engine.transform(partial)
except texform.TransformError as exc:
    print(f"{type(exc).__name__}:", exc)
```

```text
ParseError: parse produced an incomplete document
  diagnostics: [{'kind': 'raw-expected-found', 'message': 'Missing superscript content', 'span': {'start': 2, 'end': 2}, 'expected': ['superscript content'], 'found': None, 'contexts': []}]
  partial document recovered: True
has_errors: True
TransformError: cannot transform a document with parse errors
```

The two classes answer different questions, which is why they stay distinct. `normalize` could not get a complete tree out of the source, so it raises `ParseError` and carries the diagnostics and the partial document — a corpus loop can bucket the input for repair instead of discarding it. `transform` was handed a tree that does not meet its precondition, so it raises `TransformError`, the same class you get for a foreign document. In JavaScript the matching classes are `TexformParseError` (with the same `diagnostics` and `document` fields) and `TexformTransformError`. For splitting these buckets deliberately, see [Clean OCR/MER output](https://texform.dev/docs/clean-ocr-mer-output).

> Both languages name the base class `TexformError`, and catching it catches every library error. That is the safe choice in a batch loop where you care more that a formula failed than exactly why.

## If a package name is rejected, check it against `list_packages()` [#if-a-package-name-is-rejected-check-it-against-list_packages]

Package and profile names are validated at construction, so a typo fails immediately rather than quietly producing a knowledge base with a hole in it.

```python
import texform

try:
    texform.KnowledgeBase(packages=["physik"])
except texform.ConfigError as exc:
    print("ConfigError:", exc)

try:
    texform.TransformEngine(profile="canonical")
except texform.ConfigError as exc:
    print("ConfigError:", exc)

print([p["name"] for p in texform.list_packages()])
```

```text
ConfigError: unknown package: physik
ConfigError: unknown transform profile: canonical
['ams', 'base', 'bboldx', 'boldsymbol', 'braket', 'color', 'physics', 'textmacros']
```

The profile names are `authoring`, `faithful`, `corpus`, and `equiv`.

## If `\ket` and `\bra` normalize the wrong way, check `braket` against `physics` [#if-ket-and-bra-normalize-the-wrong-way-check-braket-against-physics]

`list_packages()` reports the whole catalog, but the default knowledge base loads the default runtime set, which consists of: `base`, `ams`, `braket`, `textmacros`, `bboldx`, `boldsymbol`, and `color`. `physics` is left out because it defines `\bra`, `\ket`, and friends with different semantics than `braket` does, and only one of the two can own those names.

The consequence is quiet rather than loud: `\ket{\psi}` still parses cleanly by default, because `braket` claims it. You only notice when the normalized output is not the notation you expected.

```python
import texform

default = texform.TransformEngine(profile="corpus")
kb = texform.KnowledgeBase(packages=["base", "ams", "physics"])
physics = texform.TransformEngine(profile="corpus", knowledge_base=kb)

print("braket (default):", default.normalize(r"\ket{\psi}"))
print("physics         :", physics.normalize(r"\ket{\psi}"))
```

```text
braket (default): \ket { \psi }
physics         : \left \vert \psi \right \rangle
```

To get physics semantics, name the packages explicitly and include `physics`. Listing both loads without error; `physics` wins the contested names, while braket-only commands such as `\Ket` stay available.

## If an edit is rejected, check node ownership and whether the tree is read-only [#if-an-edit-is-rejected-check-node-ownership-and-whether-the-tree-is-read-only]

Two editing guards produce most failures. A node belongs to the document that created it, and a tree that came back with parse errors is read-only for its whole lifetime. Both report `EditError` in Python and `TexformEditError` in JavaScript, and both reject the edit before any tree is modified.

```python
import texform

doc_a = texform.Parser().parse(r"x + y")["document"]
doc_b = texform.Parser().parse(r"a + b")["document"]

try:
    doc_a.append_child(doc_a.root(), doc_b.create_char("z"))
except texform.EditError as exc:
    print(f"{type(exc).__name__}:", exc)

read_only = texform.Parser().parse(r"x^")["document"]
try:
    read_only.append_child(read_only.root(), read_only.create_char("y"))
except texform.EditError as exc:
    print(f"{type(exc).__name__}:", exc)
```

```text
EditError: node belongs to a different document
EditError: document is read-only
```

Build replacement content with the `create_*` constructors on the document you are editing, and it stays owned by the right tree. See [Edit a document](https://texform.dev/docs/edit-a-document) for the full workflow.

## Next steps [#next-steps]

- [Handle parse errors](https://texform.dev/docs/handle-parse-errors) — Dispatch on the three parse states and read diagnostics and spans.

- [Clean OCR/MER output](https://texform.dev/docs/clean-ocr-mer-output) — Bucket noisy recognition output into repairable and discardable input.

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

- [Transforms](https://texform.dev/docs/transforms) — Profiles, the phase pipeline, and what the transform report tells you.

