To rewrite the example in Zig (I am no Zig expert so feel free to correct/improve):
const string = []const u8; //just creating a type alias to make the signature below have string
//Zig
fn main() !void {
s = try greet();
try std.print("{s}\n", .{s});
}
//Boomla
fn main() () (error)? {
s := greet()?
println(s)
}
//Zig
fn greet() !string {
return error.NotGreetingToday
}
//Boomla
fn greet() (string) (error) {
fail "not greeting today"
}
//Zig
fn main() !void {
s = greet() catch {
// Error handler block
}
try std.print("Hello, {s}!\n", .{s});
}
//Boomla
fn main() () (error) {
s := greet() err { // Error handler block
fail err
}
println(s)
}
Both of them look very similar. Boomla uses `?` instead of `try` and `err` instead of `catch`, but the big area where they differ is that errors in Zig are enums that you can switch on - and rely on exhaustiveness checks to know that you're handling all the types of errors that could be coming from your call stack. fn main() !void {
s = greet() catch |err| switch (err) {
error.NotGreetingToday => //handle this error
};
try std.print("Hello, {s}!\n", .{s});
}
And you know that you've handled the possibilities. Maybe you even find things that are bubbling up that shouldn't be and should be handled further down the stack.With Go/Boomla, errors are often just strings and there's no way of knowing if a library has created a new error case or easily get a complete list of the errors that might be returned. In fact, since they can just be strings, it would be impossible to know since the strings could be created programatically.
If an error is bubbled up a few levels, even if you explicitly know to look for an error, you might not know what kinds of errors you should be looking for if they're just strings. If I'm saving something to a database and there's a network error, I'll likely want to retry that. If I'm saving something to a database and it's trying to save a string to an integer column, there's no sense in retrying that - it'll just keep failing. But when errors are just strings, it's hard to know what the error is ahead of time. It then becomes a game of whack-a-mole where you log the errors you get back and then create branches for the ones you've seen (and you hope no well-meaning person changes the string without realizing that you're relying on it).
Personally, I like my IDE being able to generate all the error branches that are returned from what I'm calling. I can see that I'm handling everything the way I want it to be handled. Sometimes it means that I notice something bubbling up that should be handled further down and I can just handle it there instead. While Go talks a lot about how error handling is important, it's also important to know what the error is. I've seen a lot of Go that's just `if err != nil { //all errors are the same }` (or even just returning it up the stack where you're less likely to be anticipating random strings).
It's also great because if an error is no longer returned, Zig will have a compile error so that you take out the dead code. How many people are catching exceptions or comparing error strings that aren't returned anymore?
Boomla is definitely onto something here: explicit error handling while allowing errors to be propagated up if that's what you want. I first saw this in Zig (and great languages often independently implement similar stuff). I think the fact that I can know that I'm handling all the different errors with Zig is a further improvement.