Error Handling
Option, Result, and the ? operator.
Option Type
Loon has no nil and no null. If a value might not exist, the type system makes that explicit through Option. This means you will never hit a null pointer exception at runtime, because the compiler forces you to handle the absent case.
LOON
[let found [get {:a 1} :b]]
; found is None
[let found [get {:a 1} :a]]
; found is [Some 1]Use Some to wrap a value that exists, and None when there is nothing. Looking up a missing key in a map gives you None, not some silent null that blows up later.
Result Type
When an operation can fail, Loon uses Result to represent the outcome. This is similar to Rust's approach, and for good reason: it makes error handling explicit without the downsides of exceptions.
LOON
[fn parse-int [s]
[match [try-parse s]
[Some n] [Ok n]
None [Err "not a number"]]]Ok wraps a success value. Err wraps an error. The caller always knows that this function can fail, because the return type says so.
Pattern Matching on Results
The most direct way to handle a Result is with match. You spell out what happens in both cases, and the compiler ensures you do not forget one.
LOON
[match [parse-int "42"]
[Ok n] [println [str "got: " n]]
[Err e] [println [str "error: " e]]]This is verbose, but intentionally so. When you match on a Result, you are making a deliberate decision about how to handle failure. The compiler will not let you ignore it.
The ? Operator
Matching on every Result gets tedious when you have a chain of operations that can each fail. The ? operator is Loon's answer to this: it unwraps Ok values and propagates Err values to the caller automatically.
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 immediately without running the rest of the body. If it returns Ok, the inner value gets bound to text and execution continues. This gives you the conciseness of exceptions with the explicitness of Result types.
The ? operator works on both Result and Option. On None it returns None early.
Try Blocks
Sometimes you want to use ? inside a block but catch the error locally instead of propagating it. That is what try is for. It wraps a block of code and gives you a Result back.
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 can use ? freely. If any operation fails, the whole block evaluates to that error. If everything succeeds, you get Ok with the block's final value.
Handle for Effects
For errors that need to cross handler boundaries, Loon provides the Fail effect. This bridges effect-based code with error handling.
LOON
[handle [divide 10 0]
[Fail.raise msg] [println [str "caught: " msg]]]The handler intercepts the failure and can decide what to do: log it, return a default value, retry, or propagate further.
Prefer Result and ? for local error handling. Use Fail for errors that need to cross handler boundaries.
Error Codes
When the compiler reports an error, it includes an alphanumeric code. If the message is not enough to understand the problem, run loon explain with the code to get a detailed breakdown.
SHELL
$ loon explain E0042
E0042: Use after move
A value was used after its ownership
was transferred to another binding.
...Each explanation includes the rule being enforced, a minimal example that triggers the error, and a suggested fix. These are worth reading, especially when you are learning the ownership system.