Skip to content

Error Handling

The Error type represents an error condition. An Error has two fields:

Field Type Meaning
code ErrorCode classification of the error (see ErrorCode)
msg string human-facing message (.message is an accepted alias)

Errors are created with error(code ErrorCode, message string = ""):

do main() {
mut a Error = error(.NotFound, "no such file") // code and message
mut b Error = error(.NotFound) // message defaults to ""
mut c Error = error(.Unknown, "something went wrong") // no more specific code applies
println(a)
println(b)
println(c)
}

.Unknown (ErrorCode slot 0) is not a “no error” sentinel — “no error” is nil, one level up in the (T, Error) tuple. Reading err.code at all means you are holding a real error.

An Error is usable as a local, a parameter, a return value, and a struct field. It cannot be an array element or a map key/value; to keep a collection of errors, wrap the Error in a struct, or store err.code / err.msg.

Functions that may fail return a (T, Error) tuple; these are fallible functions. Destructuring is required; single-var assignment from a fallible function is a compile error. See Return Value Handling for the full rules.

import @io
do read_file(path string) -> (string, Error) {
if !io.file_exists(path) {
return "", error(.NotFound, "file not found")
}
return "contents", nil
}
do main() {
mut contents, err = read_file("data.txt")
if err != nil {
println(err)
} otherwise {
println(contents)
}
}

Every standard library function that returns an Error sets both a specific ErrorCode and a fixed message. Only user error() calls may omit the message.

Whether an error occurred is checked by comparing to nil:

import @io
do main() {
mut content, err = io.read_file("data.txt")
if err != nil {
println("Error: ${err}") // interpolates err.msg
exit(1)
}
println(content)
}

Which error occurred is checked on err.code, either with == / != or with when:

import @io
do main() {
mut content, err = io.read_file("data.txt")
if err != nil {
if err.code == .NotFound {
content = ""
}
when err.code {
is .NotFound { println("using defaults") }
is .PermissionDenied { println("escalate") }
default { println(err.msg) }
}
}
println(content)
}

err.code is an ErrorCode, an open enum (10.5). A when on it always requires a default branch and can never be #strict.

ErrorCode is a single, program-wide enum whose variant set is open: it is assembled at compile time from a compiler-owned builtin list plus every user enum marked #error_code. The compiler owns the numbering, so cast(someErrorCode, i64) is that variant’s global slot, not a 0-based position within one enum.

Builtin variants:

Unknown (slot 0), NotFound, AlreadyExists, PermissionDenied, InvalidInput, OutOfRange, Unsupported, Timeout, Interrupted, Closed, WouldBlock, Unavailable, IoFailure, ParseFailure, EncodingFailure, ConversionFailure, NotAuthenticated, ConnectionRefused, ConnectionReset, AddressInUse, BrokenPipe, WriteZero.

The #error_code attribute marks a normal named enum as contributing its variants to the ErrorCode set:

#error_code
const PaymentErrors enum {
PAYMENT_DECLINED
PAYMENT_CANCELED
PAYMENT_EXPIRED
}
do main() {
}
  • error(.PAYMENT_DECLINED, ...), when err.code { is .PAYMENT_DECLINED ... }, and PaymentErrors.PAYMENT_DECLINED all denote the same value.
  • The enum must be plain integer-backed. String-backed enums, enums with explicit = N variant values, tagged (payload) enums, and #flags enums are rejected under #error_code.
  • Every variant name across the whole ErrorCode set must be unique; a name already present (builtin or another #error_code enum) is a compile error.
  • PaymentErrors stays usable as its own enum type. A #error_code enum value and an ErrorCode value are freely interchangeable in comparisons and assignments — they share one value space.

Certain operations produce runtime errors that terminate program execution:

  • Division by zero (integer or float)
  • Array index out of bounds
  • Map key not found
  • Invalid type conversion

Runtime errors include location information (file, line, column).