Error Handling
Ard makes failure and absence explicit in the type system:
T!Eis aResult: either an okTvalue or an errorEvalue.T?is aMaybe: either a presentTvalue or no value.tryunwraps aResultorMaybeon the success path and returns early on the failure/empty path.
Use ard/result and Maybe for detailed constructor and method reference.
Result Types
Section titled “Result Types”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 successResult::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.
Go-compatible errors
Section titled “Go-compatible errors”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 through imported calls and generated Go APIs. Go error returns become Void!Error, and (T, error) returns become T!Error. Go parameters, fields, and callback arguments typed as error also use builtin Error.
T!Str remains supported for pure Ard message errors and continues to construct 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.
Use error.error() when an API specifically needs the message as Str, or transform a whole Result with map_err. String interpolation observes Error automatically, so "request failed: {error}" remains concise without discarding identity from the value itself.
Maybe Types
Section titled “Maybe Types”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 valueMaybe::new<T>()or context-inferredMaybe::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.
The try Keyword
Section titled “The try Keyword”Use try to unwrap a Result or Maybe and return early when it is an error or empty.
With Result
Section titled “With Result”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.
With Maybe
Section titled “With Maybe”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.
Catch Blocks
Section titled “Catch Blocks”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}"}Nested Maybe Field Chains
Section titled “Nested Maybe Field Chains”A field chain through an Ard struct wrapped in Maybe can be used as a try
operand or a match subject. If any Maybe in the chain is empty, the field-chain
result is empty.
struct Profile { name: Str?, age: Int,}
struct UserWithProfile { id: Int, profile: Profile?,}
fn profile_name(user: UserWithProfile?) Str { let name = try user.profile.name -> _ { "missing profile name" } name}
fn profile_age(user: UserWithProfile?) Int { match user.profile.age { age => age, _ => -1, }}Accessing a required field such as age: Int produces Int?. An already-nullable
field such as name: Str? stays Str?, rather than becoming Str??. The
receiver is evaluated once.
This null-propagating access is contextual: it applies to field chains directly
consumed by try or match. Ordinary field access still requires an unwrapped
struct value; use map, and_then, or an explicit match elsewhere.