KUJO THE COURSE

THE KUJO COURSE · VERIFIED 1.3.1

Exceptions and recoverable errors

Catch failures at the boundary that can make a decision.

STAGE 2 / Language semantics · LESSON 11

Why this exists

A runtime failure interrupts normal execution. Recovery needs context: should the caller skip a record, stop the run, or report a configuration problem? A catch block that always prints “success” destroys that information.

Language contract

Use try and except to protect a region, and throw(value) to signal an exception. A caught error exposes diagnostic information such as its message. Parser errors happen before the program executes and cannot be caught by a try inside the malformed program.

Expected domain rejections often fit Result better because the caller is meant to inspect them. Exceptions are appropriate where normal execution cannot continue until an outer boundary decides what to do. Avoid treating every missing optional field as an exception.

Read the example

The division helper throws for an invalid denominator. The boundary catches it, checks that the expected reason occurred, and emits a stable local summary. The test would fail if an unrelated error were caught. This matters when writing negative tests: merely seeing a nonzero outcome is not enough.

Professional pattern

Catch narrowly, preserve the reason, and decide explicitly whether the operation can continue. Convert errors into a machine-readable result at the top-level command boundary, while preserving a failure exit status where the contract requires it. A JSON object containing ok:false with exit code zero can mislead a shell pipeline unless that behavior is documented.

For external operations, distinguish a retryable transport failure from invalid arguments, denied authority, malformed data, and exhausted budgets. Repeating a denied operation does not grant authority.

Common mistakes

Do not swallow assertion failures in a broad catch meant for I/O. Do not report a deliberately caught failure as successful completion of the requested work. Never fabricate a diagnostic for teaching: this course renders the real verifier output below.

Working example

func divide(a, b) {
    if b == 0 { throw("denominator must be nonzero") }
    return a / b
}
try { divide(3, 0) } except err {
    assert_equal(contains(err.message, "denominator"), 1)
    print("rejected denominator")
}
assert_equal(divide(6, 2), 3)

Run it

From the course repository root, use the pinned Kujo 1.3.1 runtime.

kujo check examples/11.kujo
kujo run --untrusted  examples/11.kujo

Captured output

rejected denominator

Break it and diagnose it

No exception boundary handles this deliberate failure, so the command must fail.

throw("denominator must be nonzero")

kujo run --untrusted  examples/11-break.kujo

Exit status: 4. Captured diagnostic:

[KUJOVM001] [vm] Runtime Error: denominator must be nonzero
  --> 0:0


Exercise

Return Result for an invalid quantity and throw for an impossible internal state. Write tests that distinguish both paths. Add a top-level boundary that preserves failure rather than converting it to a passing run.

Checkpoint

  • I catch only where a recovery decision is possible.
  • I assert the reason in negative tests.
  • I keep failed work distinguishable from success.

Verified 2026-09-06 · Official Kujo 1.3.1 release · Source contract · Download example