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.
#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:
assert(c)stops the program ifcis false. It is for invariants: conditions that would mean the program itself is wrong. It also narrows an optional, as you saw in Chapter 11.abort(message)stops the program with a message.exit(code)ends the program normally, with an exit code.
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
-
Write
fn safe_divide(a: i64, b: i64) -> i64, booland use it to divide 12 eggs between 0 and then 4 geese. -
Add a check to
parse_goosethat rejects a weight above 15 kg with the messageline N: impossibly heavy. How many other functions did you have to change? -
Rewrite the roster loader to use the
exceptionmodule instead ofreturn … from load. What is the error type?