Error Handling
JAPL treats errors as values, not exceptions. Every operation that can fail returns a Result or Option type, and the compiler ensures you handle the failure case. There are no hidden control flow surprises — you always see where errors can occur by reading the types.
Result: Success or Failure
The Result type represents an operation that either succeeds with a value or fails with an error:
type Result =
| Ok(Int)
| Err(String)
Functions that can fail return Result:
fn divide(a: Int, b: Int) -> Result {
if b == 0 { Err("division by zero") }
else { Ok(a / b) }
}
Handling Results with Pattern Matching
The most explicit way to handle errors is pattern matching:
fn main() {
match divide(10, 2) {
Ok(n) => println("10/2 = " <> show(n))
Err(e) => println("Error: " <> e)
}
match divide(10, 0) {
Ok(n) => println("10/0 = " <> show(n))
Err(e) => println("Error: " <> e)
}
}
This prints:
10/2 = 5
Error: division by zero
Transforming Results
Use map_result to transform the success value without unwrapping:
fn map_result(r: Result, f: fn(Int) -> Int) -> Result {
match r {
Ok(x) => Ok(f(x))
Err(e) => Err(e)
}
}
let doubled = map_result(divide(10, 2), fn(x: Int) { x * 2 })
-- doubled is Ok(10)
If the result is an error, the transformation is skipped and the error passes through unchanged.
Providing Defaults
When you need a concrete value regardless of success or failure, use unwrap_or:
fn unwrap_or_result(r: Result, fallback: Int) -> Int {
match r {
Ok(x) => x
Err(_) => fallback
}
}
let safe = unwrap_or_result(divide(10, 0), 0)
-- safe is 0
The ? Operator
For functions that call multiple fallible operations, the ? operator propagates errors automatically. It unwraps an Ok value or returns the Err early:
fn get_user(id: UserId) -> Result[User, AppError] with Io =
let row = Db.query_one(sql, [id])?
validate_user(row)?
Ok(to_user(row))
This is equivalent to writing nested match expressions, but far more readable. Each ? either extracts the success value or returns the error to the caller immediately.
Option: Present or Absent
Option handles the case where a value might not exist — a safer alternative to null:
type Option =
| Some(Int)
| None
fn map_option(opt: Option, f: fn(Int) -> Int) -> Option {
match opt {
Some(x) => Some(f(x))
None => None
}
}
fn unwrap_or(opt: Option, fallback: Int) -> Int {
match opt {
Some(x) => x
None => fallback
}
}
Checking Presence
Query helpers let you branch on whether a value is present without destructuring:
fn is_some(opt: Option) -> Bool {
match opt {
Some(_) => True
None => False
}
}
fn is_ok(r: Result) -> Bool {
match r {
Ok(_) => True
Err(_) => False
}
}
Two Error Strategies
JAPL provides two complementary approaches to failure:
-
Result types for expected, recoverable errors — validation failures, missing files, bad input. These are values you handle in your logic.
-
Crash/restart semantics for unexpected failures — hardware faults, corrupted state, bugs. Processes crash and their supervisors restart them. You will learn about this in the Supervision chapter.
The guiding principle: use Result for errors your code anticipates, and let supervisors handle everything else.
Next Steps
Now that you can handle errors as values, learn how to build elegant data pipelines in Pipes & Composition.