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.
// 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}))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:
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)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.
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())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.
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)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 errorsThe 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.
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()])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.
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}"))braket (default): \ket { \psi }
physics : \left \vert \psi \right \rangleTo 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.
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)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#
Dispatch on the three parse states and read diagnostics and spans.
Clean OCR/MER outputBucket noisy recognition output into repairable and discardable input.
ParsingThe knowledge base, the strictness knobs, and the parse-state model behind these errors.
TransformsProfiles, the phase pipeline, and what the transform report tells you.