Chapter 15

Errors

Goose has no exceptions, no Result type and no required error convention. It gives you three small tools and lets you pick whichever suits the failure in front of you. This chapter covers all three, from the one you have already been using to the one that is unique to Goose, and then the ways of stopping a program outright.

#Two outcomes: a trailing bool15.1

For a function that either works or does not, return the result and a bool that says which. You have seen this with parse_int, and it is the standard idiom throughout the library:

import std;

fn parse_weight(text: u8[:]) -> f32, bool {
    let w, ok = parse_flt(trim(text));
    if !ok || w <= 0.0 { return 0.0, false; }
    return w as f32, true;
}

fn main() {
    for t in ["4.5", " 5.25 ", "heavy", "-1"] {
        let w, ok = parse_weight(t);
        print("'", t, "' -> ", w, " ", ok);
    }
}
'4.5' -> 4.5 true
' 5.25 ' -> 5.25 true
'heavy' -> 0.0 false
'-1' -> 0.0 false

#Found or not: an optional15.2

For "find me the thing, if it exists", return an optional. It is a reference into the data, so nothing is copied and the caller can change the thing it found. Chapter 11 covered this in full. The two idioms cover most of the failures a program meets: a value that cannot be produced, and a thing that cannot be found.

#Failing from deep inside: return … from15.3

Both idioms are local: a function reports to its immediate caller. That is fine for one level, but awkward when a failure is discovered many calls deep, in code that has no business knowing what to do about it. With trailing bools, every function between the failure and the place that handles it must check, and pass the failure up, and declare that it might fail.

Goose's answer is the long-distance return:

return value from f;

returns value as the result of the innermost call of f that is still running, leaving every function in between. Here a roster of geese is loaded line by line, and any malformed line ends the whole load with a message:

import std;

struct Goose { name: u8[..12], weight: f32 }

fn load(text: u8[:]) -> Goose[>..], u8[] {
    var flock: Goose[>..] = [];
    var line_no = 0;
    each_split(text, '\n') {
        line_no++;
        if trim(it).len > 0 { flock.push(parse_goose(it, line_no)); }
    };
    return flock, "";
}

fn parse_goose(line: u8[:], line_no: i64) -> Goose {
    let fields = split(line, ',');
    guard fields.len == 2 else {
        return [], str("line ", line_no, ": expected name,weight") from load;
    }
    let w, ok = parse_flt(trim(fields[1]));
    guard ok else {
        return [], str("line ", line_no, ": bad weight '", trim(fields[1]), "'") from load;
    }
    return Goose { name: trim(fields[0]), weight: w as f32 };
}

fn report(text: u8[:]) {
    let flock, err = load(text);
    if err.len > 0 { print("error: ", err); return; }
    print("loaded ", flock.len, ": ", flock);
}

fn main() {
    report("Ada, 4.5\nBea, 5.25\n");
    report("Ada, 4.5\nBea\n");
    report("Ada, heavy\n");
}
loaded 2: [Goose { "Ada", 4.5 }, Goose { "Bea", 5.25 }]
error: line 2: expected name,weight
error: line 1: bad weight 'heavy'

Look at what parse_goose does not have. Its result type is a plain Goose. It has no error parameter, returns no bool, and has nothing like ? or try. The trailing block given to each_split does not mention the error either. All the error handling in the program is in two places: the guards that notice the problem, and the one if in report that deals with it.

report load each_split the block parse_goose calls go down the page return msg from load each frame in between checks a flag and returns at once; its defers run msg is built directly where report receives load's result
return … from leaves parse_goose, the block and each_split, and delivers its value as the result of load.

#How it works, and what it costs15.3.1

This is not an exception mechanism with unwind tables. Each frame between the return … from and the target gains a hidden flag, which it checks after the call. On the way out, every frame returns normally, so every data stack is restored and every defer runs:

var depth = 0;

fn inner() -> i64 { return 99 from outer; }

fn middle() -> i64 {
    depth++;
    defer depth--;              // runs even though inner never returns normally
    let v = inner();
    print("never printed");
    v
}

fn outer() -> i64 { middle() + 1 }

fn main() { print(outer(), " ", depth); }
99 0

outer returned 99, not 100: the + 1 never ran, because middle never returned normally. depth is back to 0, because middle's defer ran on the way out. On the success path, the only cost is one flag check per frame, which is why idiomatic Goose parsers use return … from for every syntax error rather than saving it for emergencies.

The value is built directly where the target's caller wants it. The str(…) message above is written straight into the place where report receives load's second result, without passing through the frames being left.

#It is checked at compile time15.3.2

A return … from load is only legal if every possible path to it runs inside a call of load. Goose compiles the whole program at once, so it can check this, and a long-distance return can never go uncaught at run time:

fn helper() -> i64 { return 1 from outer; }
fn outer() -> i64 { 5 }

fn main() { print(helper()); }
error: return from outer: no enclosing call of outer on this compile-time call path

#The exception module15.4

Naming the target function in every return … from is precise, but it ties the failing code to one caller. The standard library's exception module wraps the same mechanism in a more familiar shape. try<E> runs a function and catches anything thrown inside it with throw, as a value of type E:

import exception;

fn deep(n: i64) -> i64 {
    if n > 3 { exception::throw(str("too deep at ", n)); }
    return n * 10;
}

fn work() {
    print(deep(2));
    print(deep(5));
    print("not reached");
}

fn main() {
    exception::try<u8[]>(work) { err => print("caught: ", err); };
    print("after");
}
20
caught: too deep at 5
after

The body to run is passed by name, here work, and the handler is the trailing block. The error type is given explicitly. u8[] is owned text, which is what you want for messages, but it can be any type: an integer code, or an enum of your own errors. As with return … from, the compiler checks that every throw has a try around it on every path.

#Stopping the program15.5

Some failures should not be handled at all. Three built-ins end the program:

fn main() {
    let geese = 3;
    assert(geese > 5);
}
goose runtime error: assert failed
fn main() {
    abort("the goose escaped");
}
goose runtime error: the goose escaped

None of these can be caught, and defers do not run when they happen. The compiler knows that abort and exit never return, so a branch that ends in one needs no value, as you saw in Chapter 5.

#Choosing15.6

Situation Use
a computation that may not produce a value a trailing bool
a lookup that may find nothing an optional
a failure deep inside a parser, loader or interpreter return … from, or exception
a bug, or a condition the program cannot continue from assert or abort

#Exercises15.7

  1. Write fn safe_divide(a: i64, b: i64) -> i64, bool and use it to divide 12 eggs between 0 and then 4 geese.

  2. Add a check to parse_goose that rejects a weight above 15 kg with the message line N: impossibly heavy. How many other functions did you have to change?

  3. Rewrite the roster loader to use the exception module instead of return … from load. What is the error type?