Skip to content

Error Handling for App Authors

The conformance error codes tell you what went wrong at a template-fixture level. This page is for the other side of that: what your app actually catches, what it can read off the exception, and a worked recipe for turning that into a message an end user (not a kBars maintainer) can act on — the case that matters most if your app lets users author templates, such as a CMS.

The exception hierarchy

KbTemplate.render throws exactly two public exception types, both extending the sealed-ish base KbException:

KbException (abstract)
├── KbParseException  — the template source could not be parsed
└── KbRenderException — the template parsed fine, but a fatal error occurred while rendering it

Both carry the same three things, inherited from KbException:

  • message (String, never null) — the underlying error's message, with the template position folded in when one was captured (see below).
  • location (KbSourceLocation?) — where in the template source the failure occurred, or null when no position could be attributed.
  • errorCode (String) — the stable conformance code for this failure (for example, "KB-2001"), or "KB-0000" for an unclassified failure. This is the identifier to key any lookup table on — the human-readable message is not guaranteed stable across kBars versions, but the code is.

Catch KbException when your app doesn't care which phase failed; catch KbParseException and KbRenderException separately when it does — for example, a CMS's "preview" step might want to report parse errors inline in an editor gutter, while a render error surfaces only when the template is actually invoked against live data.

try {
  val template = KbTemplate.create(source)
  template.render(env)
} catch (e: KbParseException) {
  // source itself is malformed — surface at edit time
} catch (e: KbRenderException) {
  // source is fine; something about this render failed (unknown transform, malformed assign target, …)
}

KbTemplate.create itself never throws — parsing is deferred until the first render call, so both exception types are only ever raised from render.

What's not an exception: soft failures

Not every problem during a render is fatal. A soft failure — a type mismatch fed into a transform, a reference to a name the context doesn't have, a search transform that found nothing — is recoverable: the expression in question evaluates to nothing and the render completes normally with no exception at all. If you want visibility into these, register a KbEventListener:

val env = KbEnvironment.create(context) {
  eventListener(
    object : KbEventListener() {
      override fun onSoftFail(event: KbSoftFailEvent) {
        logger.warn("kBars soft failure ${event.category.code}: ${event.problem}")
      }
    }
  )
}

Each KbSoftFailEvent carries its own category (with a stable code, mirroring errorCode on the exception types), a problem description, the offending input as a string (when there was a single one), the transformName it happened inside (when applicable), and its own location. See Custom Transforms for the full soft-fail vocabulary and how a custom transform reports one.

The practical split: a KbEventListener is how you observe problems without interrupting the render; KbRenderException/KbParseException are how you find out the render didn't complete.

Reading a KbSourceLocation

When location is non-null, it pinpoints the failure precisely enough to show a user their own template, not a stack trace:

  • line — 1-based line number.
  • column — 0-based, counted in Unicode code points (an emoji is one column, not two, even though it's two UTF-16 chars).
  • sourceText — the verbatim failing expression, e.g. "price | divided_by: qty".
  • templateStack — the chain of templates active at the failure, outermost first. Index 0 is always KbTemplateOrigin.Root; each following entry is a KbTemplateOrigin.Partial naming the partial key entered next. A failure inside a partial three levels deep has a four-element stack; a failure in the root template alone has the single-element stack [Root].

KbException.message already folds this into a human string for you ("... (at partial 'row' line 3:12: price | divided_by: qty)"), which is enough for a log line. Building a user-facing message usually means re-deriving the pieces yourself, since the log format is meant for a developer, not an end user:

fun describeLocation(location: KbSourceLocation): String {
  val where = when (val origin = location.templateStack.last()) {
    KbTemplateOrigin.Root -> "your template"
    is KbTemplateOrigin.Partial -> "the \"${origin.key}\" partial"
  }

  return "$where, line ${location.line}"
}

Recipe: a CMS-style user-facing message

Say end users author templates in a CMS, and you want a preview pane to show something better than a raw stack trace. The shape is the same for a parse error (caught at "save"/"preview" time) and a render error (caught when the template runs against real content):

fun renderForPreview(source: String, env: KbEnvironment): PreviewResult =
  try {
    PreviewResult.Success(KbTemplate.create(source).render(env))
  } catch (e: KbParseException) {
    PreviewResult.Failure(userMessage(e, phase = "This template has a syntax error"))
  } catch (e: KbRenderException) {
    PreviewResult.Failure(userMessage(e, phase = "This template failed to render"))
  }

private fun userMessage(e: KbException, phase: String): String {
  val location = e.location ?: return "$phase. (${e.errorCode})"
  val where = describeLocation(location)

  return "$phase in $where: ${location.sourceText}"
}

sealed interface PreviewResult {
  data class Success(val output: String) : PreviewResult
  data class Failure(val message: String) : PreviewResult
}

Two things worth building in from the start, not bolting on later:

  • Key off errorCode, not message. If you ever want to localize these messages, or offer different wording per failure kind ("unknown transform" vs. "unterminated block"), branch on errorCode — it's the part of the contract that's stable across kBars releases.
  • Don't show e.cause to the end user. The underlying Throwable (an ANTLR parse error, an IllegalArgumentException from deep in the renderer) is a developer-facing detail. Log it; don't put it in the CMS preview pane.

Where to next

  • Custom Transforms — the soft-fail vs. throw decision from inside a transform you write yourself, which is the other half of this contract.
  • Environment Properties — the builder DSL eventListener is configured through.
  • API Reference — the full Dokka reference for KbException, KbSourceLocation, and KbSoftFailEvent.