Docs/Language fundamentals

Error handling

Make failure part of a function’s interface and choose how callers recover.

On this page

An error-returning call needs an explicit handling decision. The caller can recover, stop its current work, or pass the error to its own caller.

Declare a failure#

A named error type gives a function a small set of failure codes:

Valk
error QuantityError (not_positive)

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

fn main() {
    let quantity = checked_quantity(-2) !? 1
    println(quantity) // 1
}

!QuantityError declares that the function may throw that error. throw .not_positive exits through its error result. The caller uses !? 1 to recover with a replacement quantity.

Handle the problem where you have context#

An error block can explain the failure and leave the current operation. This example does not continue with invalid input:

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

When a call's value is used, its error branch must supply a replacement or leave the flow with return, throw, break, continue, or a non-returning call such as panic.

Pass an error to your caller#

Use !> when the current function cannot usefully recover:

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
}

The enclosing function must declare a compatible error type. Passing an error preserves its code and payload.

Attach details to an error#

Payloads carry information a caller needs to make a decision or explain the problem:

Valk
error NameError (empty) payload {
    message: String
}

fn check_name(name: String) !NameError {
    if name.is_empty() {
        throw .empty { message: "A name is required." }
    }
}

fn main() {
    check_name("") ! {
        println(E.message)
    }
}

Inside a handler, E is the caught error. E.code identifies its code, and payload fields are available by name. Required payload fields must be supplied when an error is thrown; a field with an explicit default may be omitted.

Choose the right operator#

Form On failure
call() !? value Use a fallback value
call() ! { ... } Run a handler
call() !> Return the error to the caller
call() !! Panic
call() _ Discard the error and the return value

Use a panic when failure means the program cannot continue sensibly. Use _ only when ignoring the outcome is intentional. Neither is a substitute for handling expected bad input.

A missing nullable value is a separate case: use ?? for a null fallback, as described in Values & types.