Error Handling
Errors are handled using the result type, which stores either a value or an
error. Result types look like this: T except Error, where T is the type of
the value we want store if the operation was successful and Error is the type
of the error stored in the result if the operation failed. Results are used when
returning from a function that might fail:
enum ResourceError {
NOT_FOUND,
ACCESS_DENIED,
}
func load(name: String) -> String except ResourceError {
if name == "recipe" {
return "cookies";
} else if name == "topsecretfile" {
return ResourceError.ACCESS_DENIED;
} else {
return ResourceError.NOT_FOUND;
}
}
func main() {
println(load("recipe")); # "cookies"
println(load("topsecretfile")); # ResourceError.ACCESS_DENIED
println(load("keys")); # ResourceError.NOT_FOUND
}
Try Statements
The value in a result can be accessed using try statements. A try statement
unwraps the result if the operation was successful and stores the value in a
variable. If there was an error, the block is skipped.
use std.file.File;
func main() {
try file in File.open("somefile.txt", File.Mode.READ) {
println(file.read_u8());
}
}
The error value can be accessed inside an except branch:
use std.{file.File, io.Error};
func main() {
try file in File.open("somefile.txt", File.Mode.READ) {
println(file.read_u8());
} except error: Error {
print("failed to open: ");
println(error);
}
}
If you don’t care about the error value, add an else branch instead:
use std.file.File;
func main() {
try file in File.open("somefile.txt", File.Mode.READ) {
println(file.read_u8());
} else {
println("failed to open");
}
}
try statements also work with optionals:
func access(array: [i32; 3], index: i32) -> ?i32 {
if index >= 0 && index < 3 {
return array[index];
} else {
return none;
}
}
func main() {
var array: [i32; 3] = [5, 3, 9];
try value in access(array, 4) {
println(value);
} else {
println("out of bounds access");
}
}
Try Expressions
Often, we don’t want to handle an error directly where it’s produced. Instead, we want to let it bubble up the call chain so it can be handled by a function further up that has the necessary context. We can do this using try statements like this:
use std.{file.File, io};
func read_one_line(file_name: StringSlice) -> String except io.Error {
try file in File.open(file_name, File.Mode.READ) {
try line in file.read_line() {
return line;
} except error: io.Error {
return error;
}
} except error: io.Error {
return error;
}
}
However, this quickly becomes very verbose. A better alternative is to use try
expressions, which evaluate a result to its value on sucess and return the error
from the function on failure:
func read_one_line(file_name: StringSlice) -> String except io.Error {
var file = try File.open(file_name, File.Mode.READ);
var line = try file.read_line();
return line;
}
Unwrapping
If you’re absolutely sure that an operation is always successful, you can
recklessly unwrap the returned result or optional. unwrap turns a result
into a value if the operation was successful, otherwise it terminates the
program:
use std.file.File;
func main() {
var file = File.open("somefile.txt", File.Mode.READ).unwrap();
}
If the file somefile.txt doesn’t exist, isn’t accessible by the program, or
can’t be read for any other reason, the program exits using a panic.
Panics
Some errors are not recoverable and the only thing we can do is to terminate the
program. In Banjo, we use the panic function to do this. panic prints an
error message together with a stack trace to the console before exiting, which
is useful for finding and fixing the error. There are many situations where our
program has no choice but to panic: Accessing an array out of bounds, running
out of memory, failing to initialize a critical subsystem, etc.
For example, let’s see what happens when we try to decode an invalid UTF-8 sequence into a string:
func decode_utf8(slice: Slice[u8]) -> String {
var string = StringSlice.new(slice.data(), slice.length());
return String.from(string);
}
func main() {
var data: Slice[u8] = [0xAA, 0xBB, 0xCC, 0xDD];
var string = decode_utf8(data);
}
Compiling and running the program prints something like this:
$ banjo run
panic: invalid utf-8 sequence
stack trace:
#001 0x0000000002111125 panic
#002 0x0000000002109945 panic_invalid_utf8
#003 0x0000000002121092 StringSlice.new
#004 0x0000000002131540 decode_utf8
#005 0x0000000002131669 main