Skip to content

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 a KB-5xxx event 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.json also 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 *-errors case across conformance/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/name and @local/name targets 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 to false — 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 advances column by exactly one, even though it occupies two UTF-16 Chars 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\n sequence advances line exactly once, not twice — a port that counts \n occurrences in \r\n-normalized source, or advances once per character of the pair, reports the wrong line for every failure past the first \r\n in the template. column always 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-5007 at 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.