ilusm.dev
Guide

Error Handling

ilusm has three error primitives - try, err, and asr - plus optional chaining for safe nil access. No exception classes, no try/catch blocks.

err(msg) - raise an error

Call err(msg) to raise a runtime error with a string message. Execution stops at that point unless caught by try.

validate(x) =
    if x < 0: err("value must be non-negative")
    x * 2

validate(5)    # 10
validate(-1)   # raises "value must be non-negative"

try(f) - catch errors

try takes a zero-argument function and calls it. If the function raises, try catches the error and returns it. Always returns {val, err}:

  • On success: {val: result, err: nil}
  • On failure: {val: nil, err: "error message"}
load() =
    err("file not found")

r = try(load)
prn r.err   # "file not found"
prn r.val   # nil

Use a lambda to wrap code with arguments:

r = try(\() validate(-1))
if r.err:
    prn $"error: {r.err}"
| prn $"result: {r.val}"

Check success with the err field:

use fs

r = try(\() fs.rd("config.json"))
if r.err:
    prn "config not found, using defaults"
| cfg = jsn.dec(r.val)

Common pattern: early exit on error

use fs
use jsn

load_config(path) =
    r = try(\() fs.rd(path))
    if r.err: err($"cannot read config: {r.err}")
    r2 = try(\() jsn.dec(r.val))
    if r2.err: err($"invalid JSON in config: {r2.err}")
    r2.val

cfg = load_config("config.json")

Common pattern: result with default

r = try(\() risky_operation())
value = if r.err: default_value | r.val

asr(cond) - assertions

asr(cond) raises if cond is falsy. Optionally pass a message as the second argument.

asr(1 == 1)               # passes silently
asr(2 + 2 == 4, "math broken")   # passes

asr(fls)                  # raises "assertion failed"
asr(nil, "expected value") # raises "expected value"

asr is primarily used in tests with the test module, but you can use it anywhere you want a hard invariant.

Optional chaining - safe nil access

Use ?. and ?[ to access fields or indices without crashing when a value is nil. Returns nil instead of raising.

response = net.get("https://api.example.com/user")
data = jsn.dec(response.txt)

# Safe - returns nil if any step is nil
email = data?.user?.contact?.email

# Safe index access
first_tag = data?.tags?[0]

# Combine with or for defaults
host = cfg?.db?.host or "localhost"

Error messages from modules

Most stdlib functions that can fail raise an error string you can catch with try:

use fs

r = try(\() fs.rd("missing.txt"))
prn r.err   # e.g. "open missing.txt: no such file or directory"