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.

topo/code/errors.nx
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
}
topo/code/errors.expected
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.

And when what you want is a field or a method of the value inside, ?. reaches through: user?.name is null when user is and the name as an optional otherwise, so user?.name.len orelse 0 is one line where an if let would be four.

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.

Exercises#

The compiler grades these: nx topo 6 writes them into a directory, nx topo check runs them. Or do them here: the code is editable, Run runs it with the compiler in the page (it loads on the first run and never sends your code anywhere), Check grades it as nx topo check would, and Reset puts the exercise back. Ctrl+Enter runs.

fill_orelsefill in the blank

Replace ??? so a missing position falls back to zero.

fill_orelse.nx
fn find(xs: []i32, wanted: i32) -> ?usize {
    for x, i in xs { if x == wanted { return i } }
    return null
}

fn main() {
    let xs: [3]i32 = [4, 8, 15]
    println("{} {}", .{find(xs[..], 15) ??? 0, find(xs[..], 16) ??? 0})
}
The recorded output
2 0
Hints
  • one keyword gives an optional a default
Solution
fn find(xs: []i32, wanted: i32) -> ?usize {
    for x, i in xs { if x == wanted { return i } }
    return null
}

fn main() {
    let xs: [3]i32 = [4, 8, 15]
    println("{} {}", .{find(xs[..], 15) orelse 0, find(xs[..], 16) orelse 0})
}

write_letterwrite it

Fill in letter so the tests pass: 90 and up is "A", 80 and up "B", 70 and up "C",

anything lower "F"; a score under 0 or over 100 is an error.

write_letter.nx
import std.testing

error Bad { Negative, TooHigh }

fn letter(score: i32) -> Bad![]u8 {
    ???
}

fn main() {
    let scores: [6]i32 = [95, 85, 72, 40, 101, -3]
    for s in scores {
        let g = letter(s) catch |e| { println("{}: {}", .{s, @errorName(e)}); continue }
        println("{}: {}", .{s, g})
    }
}

test "letters" {
    expect_eq(try letter(95), "A")
    expect_eq(try letter(85), "B")
    expect_eq(try letter(72), "C")
    expect_eq(try letter(40), "F")
}

test "the edges" {
    expect_eq(try letter(90), "A")
    expect_eq(try letter(0), "F")
    expect_eq(try letter(100), "A")
}

test "out of range" {
    testing.expect_error([]u8, letter(101), error.TooHigh)
    testing.expect_error([]u8, letter(-3), error.Negative)
}
The recorded output
95: A
85: B
72: C
40: F
101: TooHigh
-3: Negative
Hints
  • return error.TooHigh fails; return "A" succeeds; the error set names both cases
Solution
import std.testing

error Bad { Negative, TooHigh }

fn letter(score: i32) -> Bad![]u8 {
    if score < 0 { return error.Negative }
    if score > 100 { return error.TooHigh }
    if score >= 90 { return "A" }
    if score >= 80 { return "B" }
    if score >= 70 { return "C" }
    return "F"
}

fn main() {
    let scores: [6]i32 = [95, 85, 72, 40, 101, -3]
    for s in scores {
        let g = letter(s) catch |e| { println("{}: {}", .{s, @errorName(e)}); continue }
        println("{}: {}", .{s, g})
    }
}

test "letters" {
    expect_eq(try letter(95), "A")
    expect_eq(try letter(85), "B")
    expect_eq(try letter(72), "C")
    expect_eq(try letter(40), "F")
}

test "the edges" {
    expect_eq(try letter(90), "A")
    expect_eq(try letter(0), "F")
    expect_eq(try letter(100), "A")
}

test "out of range" {
    testing.expect_error([]u8, letter(101), error.TooHigh)
    testing.expect_error([]u8, letter(-3), error.Negative)
}

Quizno peeking

  1. What does try f() do when f returns an error?
    • panics with the error's name
    • returns that error from the current function
    • turns the error into null
    Answer

    returns that error from the current function. try hands the error to the caller, whose return type must allow it; catch is the form that handles it here.

  2. When does a defer statement run?
    • right away
    • only when the function returns an error
    • when the enclosing scope ends, however it ends
    Answer

    when the enclosing scope ends, however it ends. defer runs at scope exit on every path; errdefer runs only on the error path.