Skip to content

Error Handling

Ard makes failure and absence explicit in the type system:

  • T!E is a Result: either an ok T value or an error E value.
  • T? is a Maybe: either a present T value or no value.
  • try unwraps a Result or Maybe on the success path and returns early on the failure/empty path.

Use ard/result and Maybe for detailed constructor and method reference.

Result types represent operations that can succeed or fail. They are written as ValueType!ErrorType:

fn divide(a: Int, b: Int) Int!Str {
match b == 0 {
true => Result::err("division by zero")
false => Result::ok(a / b)
}
}

Create results with:

  • Result::ok(value) for success
  • Result::err(error) for failure

Handle a result with match when both branches need local behavior:

use go:fmt
let result = divide(10, 2)
match result {
ok(value) => fmt::Println("Result: {value}")
err(message) => fmt::Println("Error: {message}")
}

Or use methods such as .or(...), .expect(...), .map(...), and .and_then(...) for concise transformations. See ard/result for the full API.

Maybe types represent values that may be absent. They are written with ? or as Maybe<T>:

struct User {
name: Str,
id: Int,
}
fn find_user(id: Int) User? {
match id == 42 {
true => Maybe::new(User{name: "Alice", id: 42})
false => Maybe::new()
}
}

Create Maybe values with:

  • Maybe::new(value) for a present value
  • Maybe::new<T>() or context-inferred Maybe::new() for an absent value

Handle a Maybe with match when you need both branches:

use go:fmt
match find_user(42) {
user => fmt::Println("Found user: {user.name}")
_ => fmt::Println("User not found")
}

Or use methods such as .or(...), .expect(...), .map(...), and .and_then(...) for concise transformations. See Maybe for the full API.

Use try to unwrap a Result or Maybe and return early when it is an error or empty.

When trying a Result, the enclosing function must return a compatible Result unless you provide a catch block.

fn calculate() Int!Str {
let x = try divide(10, 2)
let y = try divide(x, 3)
Result::ok(y + 1)
}

If either call to divide returns err, calculate returns that error immediately.

When trying a Maybe, the enclosing function must return a Maybe unless you provide a catch block.

fn get_user_name(id: Int) Str? {
let user = try find_user(id)
Maybe::new(user.name)
}

If find_user returns none, get_user_name returns none immediately.

A catch block handles the failure or empty case and returns early from the enclosing function with the catch block’s value.

fn parse_number(raw: Str) Int!Str {
match raw == "abc" {
true => Result::err("not a number")
false => Result::ok(42)
}
}
fn display_number(raw: Str) Str {
let num = try parse_number(raw) -> err {
"Failed to parse: {err}"
}
"Number is: {num}"
}

For Maybe, use _ because the absent case carries no payload:

fn user_display(id: Int) Str {
let user = try find_user(id) -> _ {
"Unknown user"
}
"User: {user.name}"
}

For simple Result error transformations, the catch handler can also be a function reference:

fn format_error(msg: Str) Str {
"Error: {msg}"
}
fn process() Str {
let value = try parse_number("abc") -> format_error
"Value: {value}"
}

try can be applied to a chained property access. If any Maybe in the chain is empty, the catch block runs:

struct Profile {
name: Str?,
}
struct UserWithProfile {
id: Int,
profile: Profile?,
}
fn profile_name(user: UserWithProfile?) Str {
let name = try user.profile.name -> _ { "missing profile name" }
name
}