Error Handling in Rust and Go
Author: Alexander Avery
Posted: Sat | Mar 27, 2021
computer-scienceHandling errors in popular languages
The first three years I spent programming, the only error handling I knew was the exception. Raising, throwing, or catching exceptions was the way I handled bad input in my programs.
It wasn’t until I took a course using C++ that I saw other methods of signaling errors. C++ has exceptions, but we often used C libraries that would return error codes as integers. I thought this was odd, but put up with the constraints of the “old language”.
Soon after, I picked up Go and Rust, and early on I was caught off guard by their seemingly antiquated ways of handling errors. I only had this opinion because of how common exceptions are in “modern” languages. The following examples are not meant to completely explain error handling in each language, but to give examples I find interesting.
Error handling in Go
Without covering panic, defer, and recover, error handling in Go takes a simple approach, building on multiple return values. This seems like a natural evolution from error handling in C. In Go you can return a value and error interface from one function, this avoids functions with side effects, which is typical of C.
Some Go newcomers find error handling repetitive, and hope for alternatives that don’t require typing the same error check hundreds of times in a program.
func MyFunction() ([]int, error) {
res, err := DoWork();
if err != nil {
return nil, err;
}
return []int{res, res, res}, nil
}
You can see it’s basic, and doesn’t have many variations from what’s shown above. I was annoyed at first by the repetition, but lately, am really fond of the simplicity. It isn’t more to type compared to exceptions, and finds what C++ and Java missed in their implementations. Potential errors are explicit, and build off the simple concept of return values. This is a solid reason Go could be easy to pick up for complete beginners.
Error handling in Rust
Error handling in Rust is a cousin to Go on the evolutionary tree. Again, it’s handled through return values, but only requires one per function. I think Rusts solution is elegant, and have been enjoying using it.
Rust builds on it’s enums to handle errors, namely, the Result<T, E>
enum with the Ok(T)
and Err(E)
variants.
The first thing you’ll learn when reading the Rust book is how to handle this return type with match, but I’ll list a few of my favorite ways to handle errors below:
fn my_function(n: i32) -> Result<i32, &'static str> {
let r = possible_error(n)?;
Ok(r)
}
The ?
token propagates the error up the stack if possible_error
returns the Err
variant. If it returns the Ok
variant, it’s value is returned from the expression.
Unwrap and expect are other methods that allow you to panic concisely when Err
is returned from a function.
fn my_function() -> i32 {
let n = possible_error().expect("special panic message");
n * 2
}
Instead of catching Err
in a match arm and panicking, calling expect will panic with the &str
passed as the message, while unwrap panics with a message provided by the value in Err
.
There are other interesting methods related to unwrap and expect I haven’t used yet which appear to be for testing:
expect_err
returns the value inErr
, or panics with the passed message & content ofOk
if it is theOk
variantunwrap_err
does exactly what you might expectunwrap_or_default
returns the contained value onOk
variants, or the default value for that type onErr
variants.
Final thoughts
I don’t know enough to say if these methods are superior to exceptions, or try/catch blocks. All I know is that I currently enjoy handling errors in this manner. Why? Maybe because I appreciate that it builds off of existing language features, rather than requiring a separate solution. I hope you appreciate it too.
Next: Considering Debian and Arch
Previous: Saving Time With the Unix Philosophy
>> Home