GuidesTroubleshooting

Troubleshooting

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

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#

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.

JavaScript
// 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#

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:

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}))
Output
ConfigError: invalid normalize overrides: rewrite.enable: unknown field `enable`, expected `enabled` or `max_iterations`
a \over b
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)
Output
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#

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())
Output
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#

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

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()])
Output
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#

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}"))
Output
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#

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)
Output
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 for the full workflow.

Next steps#

Last updated