On this page
Functions Error sets and error unionstry, catch, match
Optionals
defer and errdefer
Panics
Functions and errors
Nexium has no exceptions. A function that can fail says so in its return type, the caller sees it there, and the compiler makes sure every failure is either handled or passed on. Optionals do the same for "there might not be one". This chapter is those two types and the five words that go with them: try, catch, orelse, defer, errdefer.
error Parse { Empty, NotANumber, TooBig }
/// A decimal number from text, with the ways it can fail named.
fn parse_small(text: []u8) -> Parse!u8 {
let t = text.trim()
if t.len == 0 { return error.Empty }
var value: u32 = 0
for c in t {
if c < '0' or c > '9' { return error.NotANumber }
value = value * 10 + (c - '0') as u32
if value > 255 { return error.TooBig }
}
return value as u8
}
/// Optionals: a value or `null`; here, the position of a byte.
fn find_byte(haystack: []u8, needle: u8) -> ?usize {
for c, i in haystack { if c == needle { return i } }
return null
}
/// `try` hands an error to the caller; the caller's return type must allow it.
fn sum_of(a: []u8, b: []u8) -> Parse!u32 {
let x = try parse_small(a)
let y = try parse_small(b)
return x as u32 + y as u32
}
/// `defer` runs when the scope ends, however it ends.
fn with_cleanup(text: []u8) -> Parse!u8 {
println(" open", .{})
defer println(" close", .{})
errdefer println(" (rolled back after an error)", .{})
let v = try parse_small(text)
println(" parsed {}", .{v})
return v
}
fn main() {
// catch handles the error; the block's value stands in for the missing one
for input in ["42", "", "4x", "999"] {
let v = parse_small(input) catch |e| {
println("{}: {}", .{input, @errorName(e)})
continue
}
println("{}: ok {}", .{input, v})
}
// match on the error union, one arm per named error
match parse_small("300") {
error.TooBig => println("too big for a u8", .{}),
error.Empty => println("empty", .{}),
error.NotANumber => println("not a number", .{}),
v => println("value {}", .{v}),
}
// orelse gives an optional a default; `.?` insists (and panics on null)
let at = find_byte("nexium", 'x') orelse 99
let missing = find_byte("nexium", 'z') orelse 99
println("x at {}, z at {}", .{at, missing})
if let i = find_byte("nexium", 'm') { println("m at {}", .{i}) }
// errors travel through `try`
println("{}", .{sum_of("1", "2") catch 0})
println("{}", .{sum_of("1", "two") catch 0})
_ = with_cleanup("7") catch 0
_ = with_cleanup("seven") catch 0
}
42: ok 42
: Empty
4x: NotANumber
999: TooBig
too big for a u8
x at 2, z at 99
m at 5
3
0
open
parsed 7
close
open
(rolled back after an error)
close
Functions#
fn parse_small(text: []u8) -> Parse!u8 {
A function names its parameters with their types and its return type after ->. There are no default arguments and no overloading: one name, one signature. A function with no -> returns nothing. Parameters are borrowed: the caller keeps what it passed (chapter 8 has the whole rule).
Functions live at the top level of a file or inside an impl block (chapter 7); there are no nested function declarations, closures fill that role (|[captures] x: i32| -> i32 { x + 1 }). The order of declarations in a file does not matter.
Error sets and error unions#
error Parse { Empty, NotANumber, TooBig }
An error declaration names a set of errors. Parse!u8 is an error union: a u8, or one of Parse's errors. !u8 without a set name accepts any error at all, which is the right type for main and for code that only forwards what it gets. A named set is a contract: parse_small can fail these three ways and no other, and return error.Empty is checked against the set.
Errors are values with a name and no payload. @errorName(e) gives the name as text; the predefined ones (NotFound, IoError, InvalidInput, Overflow, ...) are what the builtins and the standard library return.
try, catch, match#
let x = try parse_small(a)
try e is "unwrap the success value, and if it is an error, return that error from this function right now". It only compiles inside a function whose return type can carry the error, which is how the compiler makes forgetting impossible: an unhandled !T in statement position is an error too (unhandled error: this expression has type ...).
let v = parse_small(input) catch |e| {
println("{}: {}", .{input, @errorName(e)})
continue
}
catch handles the failure where it happens. The handler receives the error as |e| and must produce a value of the success type, or leave: continue, break and return are all allowed there, and catch 0 is the short form when a default is all you need. match on an error union has an arm per named error and a binding arm for the success value; the compiler checks that no member of the set is left out, and else => ... takes whichever errors the other arms did not name.
Optionals#
fn find_byte(haystack: []u8, needle: u8) -> ?usize {
?usize is a usize or null. orelse supplies a default, or leaves (orelse return null, orelse continue); if let i = opt { } runs its block only when there is a value; opt.? insists there is one and panics otherwise, for the places where you have already checked. A number literal where a ?T is expected is wrapped for you, so find_byte(...) orelse 99 reads as it should.
defer and errdefer#
println(" open", .{})
defer println(" close", .{})
errdefer println(" (rolled back after an error)", .{})
defer stmt runs the statement when the enclosing scope ends, however it ends: falling off the end, return, an error propagated by try. Several defers run in reverse order. It is how a file gets closed and a lock gets released next to the line that opened or took it. errdefer runs only when the scope is left through an error, for undoing partial work.
The program shows both: the second call fails inside try, the errdefer fires, then the defer fires, then the error reaches main's catch.
Panics#
A panic is not an error value. It is the program stopping with a message and a location: an index out of bounds, an integer overflow, an opt.? on null, a panic("...") you wrote, an expect in a test that failed. In a plain program it exits with code 101; inside a library shipped with nx ship it becomes an error code the host receives (chapter 20); the compiler tracks which functions can panic at all (chapter 15).
The two mechanisms divide the world cleanly: errors are for what a correct program expects to happen (a missing file, a malformed input), panics for what it does not.
Next: structs, enums and traits.