ludic/docs/language/control/kw-try.md
Orkuncakilkaya 0d1b09e4f0
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m14s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 19s
feat(errors): recoverable failures as values — try/else over ok/err results (#46)
A fallible function returns a `result` value, built with ok(payload) on success
or err(message) on failure. The caller recovers a value with `try EXPR else {
… }`: on ok the whole expression is the payload; on err the else block runs —
with the failure message bound to `error` — and its trailing expression supplies
the fallback. It is a plain branch on the result's tag: no exceptions, no hidden
control flow, nothing unwinds. is_ok(r) / is_err(r) classify without unwrapping.

Payloads are any i32-width scalar (int/fixed/bool/entity). The feature is
additive and only kicks in when ok/err/try are used, so untouched programs
compile byte-identically (verified) and the C-free bootstrap fixpoint holds.
Complements panic/assert from #8 (the unrecoverable half). The optional
top-level frame `recover` stays deferred (needs a frame-abort mechanism); the
full tagged-union/any generalization is tracked in #1.

Adds the `try` keyword and ok/err/is_ok/is_err builtins across the compiler,
the vocabulary header, JetBrains + TextMate/VSCode grammars, the docs inventory
and pages, examples/library/recover.ludic, and a regression case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 15:04:14 +03:00

2 KiB

id name category kind tokens sig tip order
kw-try try control keyword try try EXPR else { … } Recover a fallible result as a value, with a fallback — no exceptions, no unwinding. 12

A try EXPR else { … } expression recovers a fallible operation without exceptions or unwinding. EXPR evaluates to a result — a value carrying either a success payload (from ok) or a failure message (from err). When it is ok, the whole try expression is that payload and the else block is skipped. When it is err, the else block runs and its trailing expression supplies the fallback value; inside the block the failure message is bound to error, so you can log or branch on it. It is a plain branch on the result's tag — the happy path stays one line, there is no hidden control flow, and nothing unwinds the stack.

The payload is an i32-width scalar (int, fixed, bool, or entity) — the common case of "a number or nothing". For richer payloads and full pattern-matching over tagged unions, see the type-system work (#1).

program SaveLoad {
  # a fallible op returns a result: ok(payload) or err(message)
  function load_score(slot: int) -> result {
    if slot == 1 { return ok(4200) }
    return err("no save in that slot")
  }

  test "ok yields the payload, err falls back" {
    let a = try load_score(1) else { 0 }      # 4200
    let b = try load_score(9) else { 0 }      # 0 (fallback)
    expect_eq(a, 4200)
    expect_eq(b, 0)
  }

  test "the fallback can read the failure via `error`" {
    var logged = 0
    let score = try load_score(3) else {
      if error == "no save in that slot" { logged = 1 }
      0                                        # trailing expression = the fallback
    }
    expect_eq(score, 0)
    expect_eq(logged, 1)
  }
}