Docs/Language basics

Error handling

Declare, throw, and handle errors.

On this page

A function that can fail declares an error type, and every caller must decide what to do when it fails: use a fallback, run a handler, or pass the error on.

Declare and throw an error#

Valk
error QuantityError (not_positive, too_large)

fn checked_quantity(value: int) int !QuantityError {
    if value <= 0 : throw .not_positive
    if value > 100 : throw .too_large
    return value
}

fn main() {
    println(checked_quantity(5) !? 1)   // 5
    println(checked_quantity(-2) !? 1)  // 1
}

error Name (codes) lists the possible error codes. !QuantityError after the result type says the function may throw one; throw .code leaves the function with that error.

Handle an error#

Form On failure
call() !? value Use a fallback value
call() ! { ... } Run a handler
call() ! return Run one statement
call() !> Pass the error to your caller
call() !! Panic with the error
call() _ Ignore the error (and the result)

Use !! only when failure means a bug, and _ only when the outcome really does not matter.

Fall back to a value#

Valk
fn main() {
    let port = "abc".to(int) !? 8080
    println(port)  // 8080
    let retries = "x".to(int) !? <{
        println("Invalid retries, using 3")
        return 3
    }
    println(retries)
}

A <{ ... } block runs several statements before giving the fallback. !? binds tighter than comparisons, so count() !? 0 == 3 means (count() !? 0) == 3.

Run a handler#

Valk
fn main() {
    let quantity = "many".to(int) ! {
        println("Enter a whole number, such as 3.")
        return
    }
    println("Quantity: %quantity")
}

When the call's value is used, the handler must leave the flow: return, throw, break, continue or panic.

Pass an error to the caller#

Valk
fn doubled_input(text: String) int !SyntaxError {
    let value = text.to(int) !>
    return value * 2
}

fn main() {
    let result = doubled_input("21") ! {
        println("Could not parse the input.")
        return
    }
    println(result) // 42
}

!> returns the same error, code and payload, from the current function. Its own error type must include that error.

Check which error happened#

Inside a handler, E is the error and E.code its code:

Valk
error LoadError (missing, denied)

fn load(name: String) String !LoadError {
    if name == "secret" : throw .denied
    throw .missing
}

fn main() {
    load("secret") ! {
        match E.code {
            E.missing => println("not found")
            E.denied => println("access denied")
        }
    }
    load("x") ! {
        if E.code == E.missing : println("missing")
        println("code: %{E.code}")  // code: missing
    }
}

An error code converts to its name as text. error_is(E.code, missing, denied) checks for any of several codes.

Attach details to an error#

A payload carries data along with the code:

Valk
error NameError (empty, too_long) payload {
    message: String
    limit: uint (0)
}

fn check_name(name: String) !NameError {
    if name.is_empty() : throw .empty { message: "A name is required." }
    if name.length > 10 : throw .too_long { message: "Too long.", limit: 10 }
}

fn main() {
    check_name("") ! println(E.message)
    check_name("Bartholomew the Great") ! println("%{E.message} Max %{E.limit}")
}

Payload fields without a default must be set on every throw.

Combine error types#

extends includes the codes of other error types, so a function can pass their errors on:

Valk
error ConfigError (missing) extends (SyntaxError)

fn read_port(text: String) int !ConfigError {
    if text.is_empty() : throw .missing
    return text.to(int) !>   // passes SyntaxError.syntax
}

fn main() {
    read_port("") ! println("code: %{E.code}")    // code: missing
    read_port("x") ! println("code: %{E.code}")   // code: syntax
}

Built-in error types#

Type Codes Used for
AnError error A generic failure
LookupError missing, exists, range, empty Finding or storing something, such as Array.get
SyntaxError syntax Parsing text, such as "x".to(int)
SystemError failed, unsupported The OS could not answer
IterError end Ending an each over a custom type
InitError init Setup that failed

Reuse error construction#

A function marked $throw must always throw. Call it where a throw would go:

Valk
error ParseError (invalid) payload {
    message: String
    line: uint
}

fn parse_error(message: String, line: uint) !ParseError $throw {
    throw .invalid { message: message, line: line }
}

fn parse_line(text: String, line: uint) int !ParseError {
    if text.is_empty() : parse_error("empty line", line)
    return text.to(int) ! parse_error("not a number", line)
}

fn main() {
    parse_line("", 3) ! println("%{E.message} on line %{E.line}") // empty line on line 3
}

Panic#

panic("message") stops the program with exit code 1 and prints the message with the file and line. Build with -d to get a full stack trace, see Debugging.

Errors are for calls that can fail. A value that may be missing is nullable instead and uses ??.