Intro to errors in Rust
nikhilraojl
Posted on June 24, 2023
Rust unlike many other programming languages doesn't have exceptions to handle errors. What does it mean by handling an error? Let's consider a very simple program in Python that converts a string into an integer with one success case and one that can fail
Error handling in Python
num_str = "10"
parsed = int(num_str)
print("completed")
# output
10
The above conversion from string to an integer executes successfully
name_str = "john"
parsed = int(name_str)
print("completed")
# output
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'john'
The above conversion fails with a ValueError
exception. If we observe output the print
statement after the exception is not executed. An exception not handled properly will halt the program, imagine such a piece of code halting a production server and needing a restart. Below is an example with a try-except
block in Python, the exception is handled gracefully and will not halt the program.
name_str = "john"
try:
parsed = int(name_str)
except ValueError as e:
print(e)
print("completed")
# output
invalid literal for int() with base 10: 'john'
completed
This way, in Python, we can catch exceptions. Once we catch them we can then convert them to a different exception, log the exception or just ignore it.
Error handling in Rust
Coming back to Rust, let's code up a similar string to an integer conversion program.
fn main() {
let num_str: String = "10".to_owned();
let _parsed_string: i32 = num_str.parse::<i32>().unwrap();
println!("Completed");
}
TIP: You can try this directly on rust playground https://play.rust-lang.org and see the output for yourselves
Here, we are trying to parse a String
into i32
type and the program executes successfully. We will get to why .unwrap()
is needed later. For now, we just want the code to compile and the Rust compiler won't allow it if we don't at least use unwrap
. Now, what happens if we try and run this program
fn main() {
let num_str: String = "john".to_owned();
let _parsed_string: i32= num_str.parse::<i32>().unwrap();
println!("Completed");
}
# output
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', src/main.rs:3:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The program panics during execution with a ParseIntError
as it doesn't know how to convert characters in john
to an integer value. Fair enough, but how do we handle such errors gracefully and not panic?
The Result
enum
If we take a look at the parse
method on a String
type, we see that the return type is a Result
.
pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
...
}
The Result
is an enum with two variants
-
Ok(T)
-> representing success and its value -
Err(E)
-> representing error with error value
Ok, but how does a Result
enum help us in error handling? Simple, every time there is a Result
type returned from a function we need to handle both its variants, be it success or error. The Rust compiler shouts at us if we don't(try it for yourself in the Rust playground). If it is a success, we continue with the value from Ok
variant and if there is an error we can handle it with Err
variant. Below is an example of handling the error by just printing out a message
use std::num::ParseIntError;
fn main() {
let num_str: String = "john".to_owned();
let parsed_result: Result<i32, ParseIntError> = num_str.parse::<i32>();
match parsed_result {
Ok(parsed_string) => println!("Successfully parsed {}", parsed_string),
Err(_) => println!("There was an error parsing the input"),
}
println!("Completed");
}
# output
There was an error parsing the input
Completed
If we observe the output, ParseIntError
is now successfully handled by our program and the rest of the code is executed as well without panicking. This is what we were aiming for
The unwrap
method
So what is the unwrap
thing mentioned earlier? If we again take a look at our first Rust example in this blog converting 10
into an integer,
let _parsed_string: i32 = "10".to_owned().parse::<i32>().unwrap();
we did not use any match
statement to handle the Result
enum, instead, we used the unwrap
method. Yet the code compiled and returned the output successfully.
If the output from a function after its execution is of the Ok(T)
variant the value from type T
is consumed and the program continues as expected. But if the output from a function is of the Err(E)
variant the current function will panic.
If we take a look at our second code example,
let _parsed_string: i32 = "john".to_owned().parse::<i32>().unwrap();
where we tried to parse the characters in "john" into an integer using unwrap
and our program panicked.
Why do we use unwrap
if the program can still panic? unwrap
is used to quickly handle any Result
types only thinking about valid/correct cases and ignoring handling errors. Although heavy use of unwrap
is discouraged, it is very handy during development. We can always go back and handle Result
types properly at a later stage.
The ?
operator
Imagine a case where we are calling a function that returns a Result
but we don't want to handle its Err
variant manually(match
ing and returning a new Err
), instead, we would just like to forward the Err
to the calling function.
Let's consider the below example where we try to parse all elements in a Vec
and return its total sum to any function that calls parse_vec_and_sum
.
use std::num::ParseIntError;
fn main() {
let strings_vec: Vec<&str> = vec!("1", "2", "3");
let parsed_result = parse_vec_and_sum(strings_vec);
match parsed_result {
Ok(parsed_string) => println!("Successfully parsed vec to {}", parsed_string),
Err(_) => println!("There was an error parsing the input"),
}
println!("Completed");
}
fn parse_vec_and_sum(v: Vec<&str>) -> Result<i32, ParseIntError> {
let mut sum: i32 = 0;
for i in v {
match parse_int(i.to_owned()) {
Ok(parsed_integer) => {
// If parsing is successful, we just add it to total sum
sum += parsed_integer;
},
Err(e) => {
// If there is an Error, return it immediately
return Err(e);
}
};
}
return Ok(sum);
}
fn parse_int(str: String) -> Result<i32, ParseIntError> {
let x = str.parse::<i32>();
let value: i32;
match x{
Ok(v) => {value = v;},
Err(e) => return Err(e)
};
// Let's assume, for simplicity, some processing on `value` is needed after `.parse`.
// Otherwise we could have just returned value from `.prase`
return Ok(value);
}
# output
Successfully parsed vec to 6
Completed
The above example works fine, but it is very verbose to write. For such operations, we can use the handy ?
operator. This question mark operator will either consume the value if Ok(T)
or it will propagate the error by returning Err(E)
to the calling function. Let's rewrite the above program using the ?
operator
use std::num::ParseIntError;
fn main()-> Result<(), ParseIntError> {
let strings_vec: Vec<&str> = vec!("1", "2", "3");
let parsed_string = parse_vec_and_sum(strings_vec)?; // operator used here
println!("Successfully parsed vec to {}", parsed_string);
println!("Completed");
return Ok(());
}
fn parse_vec_and_sum(v: Vec<&str>) -> Result<i32, ParseIntError> {
let mut sum: i32 = 0;
for i in v {
sum += parse_int(i.to_owned())?; // operator used here
}
return Ok(sum);
}
fn parse_int(str: String) -> Result<i32, ParseIntError> {
let x = str.parse::<i32>()?; // operator used here
// Let's assume, for simplicity, some processing on `value` is needed after `.parse`.
// Otherwise we could have just returned value from `.prase` directly
return Ok(x);
}
# output
Successfully parsed vec to 6
Completed
Now, let's change the vec to something which can cause an error
...
let strings_vec: Vec<&str> = vec!("john", "2", "3");
...
# output
There was an error parsing the input
Completed
The error ParseIntError
is propagated from parse_int
-> parse_vec_and_sum
-> main
. This makes our program even less verbose and a breeze to write.
Please note, the above example is a bit drawn out, it can all be packed into a main function but it is deliberately split into multiple functions to explain error propagation
Closing words
So far we have looked at basic error handling in Rust. We have looked at
-
Result
enum which can either be successOk(T)
or failureErr(E)
- Using
.unwrap
to quickly handleResult
types - Use of
?
operator to propagate errors through the calling functions
There is a lot more to learn about errors in Rust. Take the above example using the ?
operator, we are returning only one error type, what if multiple errors are returned by each function? or What if the error in a Result
is dynamic and we don't exactly know its type? How are we to handle such scenarios? Also, there is std::error::Error
which is a fundamental trait for representing error values i.e. representing any E
in a Result<T, E>
. We need to learn a little more about this Error
trait.
Don't worry, we can always learn more. But having solid fundamentals will help in understanding more complex topics. I hope this introduction blog has helped some of you to understand errors and error handling in Rust.
Posted on June 24, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.