Skip to content

Error Sets

When a function returns !T (or bare ! for functions with no return value), the set of errors it can produce is inferred by the compiler. You do not need to declare error types manually.

The compiler analyzes your function and determines every error that can be returned, including errors propagated with try.

package main
use "fmt"
use "os"
fun load_config(path: string) !string {
file := try os.open(path) // may return os.NotFound, os.Permission, ...
defer file.close()
content := try file.readAll() // may return io.ReadError, ...
return content
}

The error set of load_config is the union of all errors from os.open and file.readAll. You never need to write this out — the compiler tracks it for you.

When you need to react differently to specific errors, use switch to match on the error variant.

package main
use "fmt"
use "os"
pub fun main() {
switch os.open("config.txt") {
.ok(file) :: {
defer file.close()
fmt.println("opened successfully")
},
.err(.not_found) :: fmt.println("file not found"),
.err(.permission) :: fmt.println("access denied"),
.err(e) :: fmt.println("unexpected error:", e),
}
}

The try keyword unwraps a success value or immediately returns the error from the current function. This keeps the happy path clean and readable.

fun process() !int {
data := try readFile("input.txt") // returns error if read fails
parsed := try parseInt(data) // returns error if parse fails
return parsed * 2 // only reached on success
}

Errors in Run are values, not exceptions. They flow through the return type, making every error path visible in the function signature.