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.

Error is Go’s error interface, expressed in Ard as a builtin trait just like other Go interfaces. Use Error::new for a message-only error:

fn validate(port: Int) Void!Error {
match port > 0 {
true => Result::ok(())
false => Result::err(Error::new("port must be positive"))
}
}

Define a struct and explicitly implement Error when an error needs structured state:

struct ValidationError {
field: Str,
message: Str,
}
impl Error for ValidationError {
fn error() Str {
"{self.field}: {self.message}"
}
}

T!Error preserves the underlying Go error value when returned through generated Go APIs. T!Str remains supported and continues to use a message-only Go error at return boundaries. Result error types are otherwise unconstrained; use any E in T!E when Go error interoperability is not needed.

Imported Go functions returning error continue to expose T!Str for compatibility. Go parameters and fields typed as error use builtin Error.

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
}