Error Handling¶
Every failure (parse, validation, quill config, backend compile) travels as a Diagnostic, and each binding raises a single error type that always carries a non-empty diagnostics list. Consumers route on a diagnostic's namespaced code, never on an exception subclass.
One thing never reaches a diagnostic: an argument the binding cannot convert at all. Python raises ValueError for a non-finite float, an int past 64 bits, a value whose type has no JSON form, or a malformed path sequence — core never sees the call.
The Diagnostic shape¶
| Field | Meaning |
|---|---|
severity |
Error (blocks its stage) or Warning (never blocks): Severity.ERROR / Severity.WARNING in Python, "error" / "warning" in JavaScript |
code |
Namespaced id; e.g. parse::missing_quill, validation::type_mismatch, quill::version_mismatch: the machine-routable identity |
message |
Human-readable text |
location |
Optional text anchor: file, line (1-based), column |
path |
Optional document-model anchor: main.recipient, cards.indorsement[0].author (the document's first card, an indorsement; card numbering) |
hint |
Optional actionable suggestion |
location (where in the source text) and path (which field in the model) are independent and may co-exist.
Catching errors¶
A multi-problem stage (validation, quill config, backend compile) reports every problem in one pass, so diagnostics may carry several entries; diagnostics[0] is the primary, and an error. A Warning after the errors is one raised before the failure that can explain it (below). The error's message follows a count-based rule: the primary message for one error, "<N> error(s): <first message>" for more, warnings not counted.
Codes, not types¶
The code namespaces are the routing surface:
parse::* · validation::* · quill::* · edit::* (mutators) · conform::* (Quill::conform warnings) · typst::* · acroform::* · pdf::* (the AcroForm stamping spine both PDF-producing backends share) · backend::* · engine::* · cli::* · runtime::* (the JavaScript runtime).
Diagnostic Codes lists every code.
Notable codes: quill::name_mismatch / quill::version_mismatch (a well-formed document paired with the wrong quill; see Versioning); engine::backend_not_found (the quill's declared backend is not registered); parse::input_too_large, which carries the two byte-sized §8 caps — document size and YAML payload size — distinguished only by its max arg, while the count caps arrive as parse::too_many_cards and parse::too_many_fields (args count, max) and an over-deep payload as parse::yaml_error_with_location.
A content write meets the content bounds under edit::*:
edit::input_too_large(size,max): markdown past 10 MiB at a revise orimportMarkdown, or a content value whose text passes it.edit::too_many_steps(count,max): a change of more than 16 384 steps.edit::too_many_marks(count,max): a text holding more marks than 64 and 4 per character of it.edit::too_many_anchors(count,max): a value holding more than 100 000 anchors.
parse::payload_not_mapping is a card-yaml block whose payload is YAML but not a mapping, usually code fenced with ~~~ in a body. It is located at the fence's line. Its args carry actual (string, number, boolean or sequence) and, when the fence has one, info, the info string.
parse::missing_kind is a card-yaml block after the root whose payload names no $kind: a card missing its kind line, or code fenced with ~~~ whose text reads as a mapping. It is located at the fence's line, and its args carry info when the fence has one.
A Typst compile classifies into four codes: typst::file_not_found (a file the quill's world refused — a missing asset is the common one), typst::unknown_variable, typst::type_error, and typst::compile for everything else, warnings included. They are a routing key only: which file was searched for, or which symbol was unknown, is read from message.
Before any compile, opening a Typst quill can refuse its plate: typst::plate_missing when the quill holds no plate.typ and declares no typst.plate_file, or typst.plate_file names no file in the quill, typst::plate_path_invalid when typst.plate_file is not a string, names no path, or names a file at a path Typst cannot load, and typst::helper_import, one per import, when any .typ file of the quill outside packages/ imports the helper package any way but #import "@local/quillmark-helper:0.2.0" as qm, whether or not the plate imports that file.
Warnings vs errors¶
Fatality is a two-value ladder: Error blocks the stage that emits it; Warning never does. There is no lint-level configuration and no warning-to-error promotion. Warnings ride the same Diagnostic currency on non-fatal channels:
- Parse warnings (e.g. a
~~~opener missing its blank line) carried on the parsed document (doc.warnings). A render does not carry them: they report the load, and a revise can change what the document holds after it. parse::dropped_construct: markdown the content cannot carry, one per construct, carryingconstructandcountinargs.constructis a raw tag's lowercase name, such asdivor an HTML table'scaption, or theqm-*tag that dropped:qm-keepfor a tag it ignores,qm-span[color]for an attribute. An HTML table reportstablewhere its block ends before its</table>,table:rowfor a row too short for it,table:textfor text outside every cell,table:sizefor the rows of a table past 100 000 cells, and<tag>[<attr>], such astd[nowrap]ortd[colspan], for an attribute it does not read other thanborder,class,scopeandstyle, which drop silently. A footnote reference past the 1 000 000 characters the copies of a document's notes share reportsfootnote:sizeand stays as its[^label]text.- A parse anchors it at the body (
main.body,cards.<kind>[<i>].body) ondoc.warnings. Arichtextfield's markdown string reports at the field (main.subject,cards.<kind>[<i>].items[0]): ondoc.warningsfromquill.parse, and fromquill.validate(doc)on a document loaded without its quill. A typedset/setAllreturns them at the field, andaddCardits fields' ahead of its body's. doc.revisereturns it in its receipt'swarnings, and the writer'srevisein the sameReceipt, anchored at the address written; Python'srevisereturns it among their dict receipt'swarnings, asDiagnosticobjects.- The writer's
reviseDocumentreturns it among its receipt'swarnings, and so does Python'srevise_document. addCardreturns it in itswarningslist, beside the placed card'sid, anchored under that card;importMarkdownreturns it beside the content, with no path.
- A parse anchors it at the body (
-
Validation warnings:
quill.validate(doc)returns every diagnostic, and its severity says whether the document renders (full rule):- An
Erroris input Quillmark cannot read as written, such as a value that is not its field's type or an enum value outsidevalues:. The render fails. - A
Warningis input no declaration claims: a card with an undeclared$kind(validation::unknown_card), a body underbody.enabled: false(validation::body_disabled), a key the schema does not declare (validation::unknown_field, whose hint names the likely fix), a stranded variant cell (validation::out_of_variant), elements pastmax:(validation::cardinality), characters past a text field'smax:(validation::over_length), and the$seedchecks. The document renders without it. A one-shot render carries these warnings onresult.warnings, ahead of the compile's; a live session's warnings do not, so an editor readsquill.validate(doc)beside them.
A line break inside a one-line
textvalue warns too (validation::line_break_folded), and renders as a space.A field the document leaves unanswered draws no diagnostic: it renders its
default:, else its blank.On the CLI,
quillmark renderandquillmark checkboth print every validation warning (CLI Reference). - Compile warnings: a backend's non-fatal diagnostics (font fallback, overfull pages), carried onresult.warnings. - Load warnings: what the Typst backend skipped loading the quill (typst::package_manifest,typst::package_entrypoint_missing,typst::path_skipped,typst::font_collection_skipped), carried ahead of the compile warnings. A render or sessionopenwhose compile fails carries them too, in the error'sdiagnosticsafter its errors: an#importof a package skipped for its manifest fails astypst::file_not_found, and only the warning says why. -backend::declined_construct: a construct the backend typesets nothing for, one per content field, carryingbackend,constructandcountinargsand the field's path. The Typst backend declinesimage: a markdown image in arichtextfield reaches no page, because no backend resolves a content image's url. The acroform backend declines every construct but the paragraph.quill.validate(doc)returns the same warnings before any render. - An
A successful render returns artifacts and a warnings list, so inspect it even on success.
Full model: ERROR.md.