Bars Specification 0.1.0 — Errors¶
How a conforming implementation reports failure, and where a failure points.
Hard fail vs. soft fail¶
Bars has two distinct failure modes, and an implementation must not conflate them:
- Hard failure — evaluation stops and an exception (or the host language's equivalent) propagates
to the caller. Reserved for conditions the template author cannot recover from at render time:
malformed source (
KB-1xxx), a reference to a transform or partial that does not exist at all (KB-2001,KB-4001,KB-4002), or a malformed assign-tag target (KB-3001,KB-3002). - Soft failure — evaluation does not stop. The failing sub-expression resolves to empty/
null, rendering continues to completion, and the failure is reported out-of-band as aKB-5xxxevent to any listener registered on the rendering environment. Reserved for conditions that arise from the data, not the template source itself: a wrong-shaped value, a missing identifier, a comparison between incompatible types, a partial-recursion overrun.
A port need not use exceptions or event listeners specifically — the mechanism is implementation-defined — but the two-tier behavior is core: a malformed template must never silently produce partial output as if it were valid, and a bad value encountered while rendering an otherwise valid template must never abort the whole render.
Human-readable message text is not normative and may differ between kBars releases or between
ports entirely; only the errorCode below is stable. Fixtures assert errorCode (and, for soft
fails, the expectedSoftFails category list), never message text.
Error code taxonomy¶
Every classified failure carries a stable KB-NNNN code, normatively defined by
conformance/error-codes.json. Codes are grouped into families by their thousands digit:
| Range | Family | Kind |
|---|---|---|
| KB-1xxx | Grammar | hard |
| KB-2xxx | Transform | hard |
| KB-3xxx | Assign | hard |
| KB-4xxx | Partials | hard |
| KB-5xxx | Soft-fail | soft |
KB-0000 is a reserved, unclassified fallback — no conformance fixture asserts it as an expected
code, and a port need not reproduce it specifically. KB-3001 (AssignTargetInvalid) is also
reserved: every currently-known malformed assign-target form is pre-empted at parse time by
KB-1001 before an implementation would ever reach the runtime check KB-3001 guards, so no fixture
exercises it. KB-1003 (NestingTooDeep) is likewise reserved: the maximum block-nesting depth is
implementation-defined (see Tunable
limits), so no portable fixture can pin the exact depth
that trips it. All three reserved codes are exempt from the coverage requirement that every other
code below is pinned by at least one fixture.
Grammar (hard)¶
KB-1001— SyntaxError. Catch-all for template source the grammar (see Lexical Structure) rejects. Fixtures:conformance/grammar/binary-operators.json,error-no-identifier.json,transform-arguments.json,transform-chain.json;conformance/env-properties/assign-errors.jsonalso triggers it for malformed assign targets, since parse-time rejection pre-empts several runtime checks.KB-1002— MissingWhitespace. The grammar requires at least one space between{{and the first token, and between the last token and}}; this fires when that whitespace is absent. Fixtures:conformance/grammar/error-missing-leading-ws.json,error-missing-trailing-ws.json,error-missing-whitespace.json.KB-1003— NestingTooDeep (reserved — see above). A template's block nesting exceeds the implementation's maximum depth, checked before the recursive-descent parser runs so a pathologically deep template fails cleanly instead of overflowing the native call stack. See Tunable limits for what binds and what varies.
Transform (hard)¶
KB-2001— UnknownTransform. A transform name in the template is not registered in the rendering environment. Fixture:conformance/partials/origin-hard-failures.json.KB-2002— TransformArity. A built-in transform was called with the wrong number of arguments; every built-in documents its required arity. Fixtures: every*-errorscase acrossconformance/transforms/*.json.
Assign (hard)¶
KB-3001— AssignTargetInvalid (reserved — see above).KB-3002— AssignTargetSubPointer. An{{= }}target carries a sub-pointer (e.g.@env/a/b); only bare@env/nameand@local/nametargets are valid — see Rendering. Fixtures:conformance/env-properties/assign-errors.json(assign-error-env-subpointer-target,assign-error-local-subpointer-target).
Partials (hard)¶
KB-4001— UnknownPartial. A static{{> key }}names a partial the registered loader does not know. Fixture:conformance/partials/hard-failures.json(unknown-static-key-is-hard-failure).KB-4002— NoPartialLoader. A{{> }}tag was encountered but no partial loader is registered at all. Fixture:conformance/partials/hard-failures.json(no-loader-registered-is-hard-failure).
See Rendering for the full partial-failure matrix these two codes sit within.
Soft-fail categories¶
KB-5001— TypeMismatch. A value or transform argument was the wrong type/shape.KB-5002— DegenerateValue. The type was correct but the value is degenerate (a zero divisor, an empty collection where one element is required).KB-5003— UnparseableInput. A string was the right type but failed to parse into the required sub-type (a malformed date string, a malformed percent sequence).KB-5004— ComparisonMismatch. A relational comparison (<,<=,>,>=) received incompatible operand types and fell back tofalse— see Data Model and Rendering.KB-5005— UnresolvedReference. An identifier ({{ name }},@env/name) resolved against neither the context nor the environment — see Data Model.KB-5006— NoMatch. A search transform (find,find_index) matched nothing; reported at informational severity since a miss is an ordinary outcome, not a defect.KB-5007— RecursionLimit. A{{> }}chain exceeded the configured maximum partial-render depth; the over-deep partial is abandoned and rendering continues — see Implementation-Defined Behavior.
Conformance fixtures assert soft-fail categories via a render/partials fixture's
expectedSoftFails field; see conformance/partials/soft-failures.json's
recursion-limit-soft-fails-and-stops case for KB-5007 specifically.
Normative fixture set: conformance/error-codes.json is the authoritative enumeration of all
seventeen codes (including the three reserved ones) — this section is its normative prose
explanation, not a substitute for it.
Source locations¶
Parse- and render-time failures carry a KbSourceLocation-equivalent position:
line— 1-based line number.column— 0-based position within the line, counted in Unicode code points — never raw UTF-16 code units, never bytes. A non-BMP character (for example, most emoji) is one code point and therefore advancescolumnby exactly one, even though it occupies two UTF-16Chars in a Kotlin/Java/JavaScript string and a variable number of UTF-8 bytes. This is the same code-point rule that governs lexing and the string transforms; a port whose native string type counts UTF-16 units or UTF-8 bytes must count code points explicitly for this field rather than trusting the host string type's native length/index operations.- A
\r\nsequence advanceslineexactly once, not twice — a port that counts\noccurrences in\r\n-normalized source, or advances once per character of the pair, reports the wrong line for every failure past the first\r\nin the template.columnalways counts from the start of the failing expression's own line, never from the start of the template. - This rule is uniform across failure kinds: a render-time soft/hard failure and a grammar-level
parse failure (
KB-1001,KB-1002) on line three of a template both report line 3, never a fixed line 1. - A failure occurring inside a loaded partial also carries a template-stack-equivalent structure
identifying which template — root or a named partial — was active. A partial reached through a
static
{{> key }}tag carries no source location at all, since there is no parsed expression to attach one to; only a dynamic{{> (expr) }}key has one. See Rendering for which error codes this affects.
Normative fixture: conformance/render/source-location.json — a non-BMP emoji before a failing
expression (pinning the code-point column rule against both UTF-16-unit and UTF-8-byte
miscounts), matched LF-only/CRLF multi-line cases (pinning the one-line-break-one-line-advance
rule), a leading-text single-line case (pinning that column resets per line), and a multi-line
KB-1001 case (pinning that parse-time failures report real source lines, not a fixed line).
Adversarial inputs¶
A conforming implementation degrades gracefully on hostile input rather than crashing:
- Legitimate nesting within the implementation's maximum block-nesting depth renders to
completion; nesting depth alone is never an error below that ceiling. Nesting that exceeds it is
a hard parse-time failure (
KB-1003), not a stack overflow or a crash — see Tunable limits. - Unterminated openers at end-of-input are rewritten by the pre-pass into literal text (see
Pre-pass normalization); they never raise a parse error. This
includes the 4-brace raw-block opener (
{{{{), whose escaped form needs two inserted\markers, not one — a single\{{escape only ever neutralizes 2 of the 4 braces, and leaving the residual{{unescaped would make the lexer re-enter it as a second, unterminated opener. - Runaway partial recursion soft-fails with
KB-5007at the depth limit (see Tunable limits); it never overflows the call stack or throws, whether the recursion is direct (a partial invoking itself) or mutual (two or more partials invoking each other in a cycle) — the limit counts total nesting depth, not per-partial-name depth.
Normative fixtures: conformance/render/torture-deep-nesting.json,
conformance/render/torture-unterminated-at-eof.json,
conformance/partials/torture-adversarial-recursion.json.