Error Handling
Klar uses two main types for error handling: ?T (Optional) for values that may be absent, and Result#[T, E] for operations that may fail with an error.
Optional Type (?T)
The optional type ?T represents a value that may or may not exist.
Creating Optional Values
let some_value: ?i32 = Some(42)
let no_value: ?i32 = NoneReturning Optional from Functions
When a function returns ?T, returning a value wraps it in Some:
fn find_value(id: i32) -> ?i32 {
if id == 1 {
return 100 // Automatically wrapped as Some(100)
}
// Implicit None return at end of function
}Unwrapping Optionals
Force Unwrap (!)
Use ! to unwrap when you're certain the value exists:
let opt: ?i32 = Some(42)
let value: i32 = opt! // 42
// Panics if None!
let empty: ?i32 = None
let bad: i32 = empty! // Runtime panicNull Coalescing (??)
Use ?? to provide a default value:
let opt: ?i32 = None
let value: i32 = opt ?? 0 // 0 (default used)
let opt2: ?i32 = Some(42)
let value2: i32 = opt2 ?? 0 // 42 (value used)Pattern Matching Optionals
fn process(opt: ?i32) -> string {
var result: string
match opt {
Some(value) => { result = "Got: {value}" }
None => { result = "Nothing" }
}
return result
}Result Type
Result#[T, E] represents an operation that either succeeds with a value of type T or fails with an error of type E.
Creating Results
let success: Result#[i32, string] = Ok(42)
let failure: Result#[i32, string] = Err("something went wrong")Returning Results from Functions
fn divide(a: i32, b: i32) -> Result#[i32, string] {
if b == 0 {
return Err("division by zero")
}
return Ok(a / b)
}Unwrapping Results
Force Unwrap (!)
let result: Result#[i32, string] = Ok(42)
let value: i32 = result! // 42
// Panics if Err!
let err: Result#[i32, string] = Err("oops")
let bad: i32 = err! // Runtime panicPattern Matching Results
fn handle_result(r: Result#[i32, string]) -> void {
match r {
Ok(value) => {
println("Success: {value}")
}
Err(message) => {
println("Error: {message}")
}
}
}Checking Result Status
let result: Result#[i32, string] = Ok(42)
if result.is_ok() {
println("Success!")
}
if result.is_err() {
println("Failed!")
}Error Propagation (?)
The ? operator propagates errors up the call stack.
With Result
fn read_config() -> Result#[Config, string] {
let content: string = read_file("config.txt")? // Propagates if Err
let config: Config = parse_config(content)? // Propagates if Err
return Ok(config)
}If read_file returns Err, the function immediately returns that error. If it returns Ok, the value is unwrapped and execution continues.
With Optional
The ? operator can also propagate None:
fn get_user_email(id: i32) -> ?string {
let user: User = find_user(id)? // Returns None if not found
let email: string = user.email? // Returns None if email is None
return Some(email)
}Combining Approaches
Converting Between Types
// Optional to Result
fn opt_to_result(opt: ?i32) -> Result#[i32, string] {
match opt {
Some(value) => { return Ok(value) }
None => { return Err("value not found") }
}
}
// Result to Optional (discards error)
fn result_to_opt(r: Result#[i32, string]) -> ?i32 {
match r {
Ok(value) => { return Some(value) }
Err(_) => { return None }
}
}Example: File Processing
fn process_file(path: string) -> Result#[i32, string] {
// Read file, propagate error if fails
let content: string = read_file(path)?
// Parse number, propagate if fails
let number: i32 = parse_int(content)?
// Process the number
return Ok(number * 2)
}
fn main() -> i32 {
let result: Result#[i32, string] = process_file("data.txt")
match result {
Ok(value) => {
println("Result: {value}")
return 0
}
Err(message) => {
println("Error: {message}")
return 1
}
}
}Example: Chained Optionals
struct User {
name: string,
address: ?Address,
}
struct Address {
city: ?string,
}
fn get_user_city(user: ?User) -> ?string {
// Each ? returns None if the value is None
let u: User = user?
let addr: Address = u.address?
let city: string = addr.city?
return Some(city)
}Best Practices
Use Optional When
- A value might legitimately be absent
- The absence is not an error condition
- You need a simple "present or not" distinction
fn find_by_id(id: i32) -> ?User {
// User might not exist - that's normal
}Use Result When
- An operation can fail
- You need to communicate why it failed
- The caller needs to handle the error
fn connect(host: string) -> Result#[Connection, NetworkError] {
// Connection might fail for various reasons
}Prefer ? Over Match for Propagation
// Good - concise
fn process() -> Result#[i32, string] {
let a: i32 = step1()?
let b: i32 = step2(a)?
return Ok(b)
}
// Verbose - only use when you need custom handling
fn process_verbose() -> Result#[i32, string] {
var a: i32
match step1() {
Ok(value) => { a = value }
Err(e) => { return Err(e) }
}
// ...
}Next Steps
- Optional - Full optional type reference
- Result - Full result type reference
- Control Flow - Pattern matching details