Error Handling

Option, Result, and the ? operator.

Option Type

Loon has no nil and no null. When a value might not exist, the type system says so out loud through Option. A null pointer exception can never reach you at runtime, because the compiler will not let the absent case go unhandled.

LOON
[let found [get {:a 1} :b]]
; found is None

[let found [get {:a 1} :a]]
; found is [Some 1]

Wrap a present value in Some, and reach for None when there is nothing to wrap. Looking up a missing key in a map hands you None, not a silent null waiting to detonate later.

Result Type

When an operation can fail, Loon carries the outcome in a Result. The approach echoes Rust's, and for the same reason: failure becomes explicit, with none of the action-at-a-distance of exceptions.

LOON
[fn parse-int [s]
  [match [try-parse s]
    [Some n] [Ok n]
    None [Err "not a number"]]]

Ok wraps a success; Err wraps a failure. The caller is never caught off guard, because the return type announces that this function can fail.

Pattern Matching on Results

The most direct way to handle a Result is match. You spell out both outcomes, and the compiler makes sure neither slips through.

LOON
[match [parse-int "42"]
  [Ok n] [println [str "got: " n]]
  [Err e] [println [str "error: " e]]]

The verbosity is the point. Matching on a Result is you deciding, in the open, what failure means here. The compiler will not let you wave it away.

The ? Operator

Matching by hand wears thin when a chain of operations can each fail in turn. The ? operator is Loon's relief: it unwraps an Ok in place and hands any Err straight back to the caller.

LOON
[fn load-config [path]
  [let text [IO.read-file path]?]
  [let data [parse-json text]?]
  [Ok data]]

If IO.read-file returns an Err, the function returns that error at once and the rest of the body never runs. If it returns Ok, the inner value binds to text and execution flows on. You get the brevity of exceptions with the candor of Result types.

Note

The ? operator works on both Result and Option. On None it returns None early.

Try Blocks

Sometimes you want ? inside a block, but you want to catch the error here rather than send it upward. That is the job of try. It wraps a block of code and returns a Result.

LOON
[let result [try
  [let a [parse-int "10"]?]
  [let b [parse-int "bad"]?]
  [+ a b]]]
; result is [Err "not a number"]

Inside a try block you may use ? without restraint. Let any operation fail and the whole block evaluates to that error; let them all succeed and you get Ok carrying the block's final value.

Handle for Effects

For errors that must travel across handler boundaries, Loon offers the Fail effect, the bridge between effect-based code and error handling.

LOON
[handle [divide 10 0]
  [Fail.raise msg] [println [str "caught: " msg]]]

The handler catches the failure and chooses its fate: log it, return a default, retry, or pass it further along.

Tip

Prefer Result and ? for local error handling. Use Fail for errors that need to cross handler boundaries.

Error Codes

Every compiler error arrives with an alphanumeric code. When the message alone leaves you guessing, run loon explain with the code for the full account.

SHELL
$ loon explain E0042
E0042: Use after move

A value was used after its ownership
was transferred to another binding.
...

Each explanation gives the rule at work, a minimal example that trips it, and a fix to try. They reward reading, above all while the ownership system is still new to you.