Error Handling in Rust
Using panic!
If your program fails in a way that's unrecoverable or there is no way to handle the error gracefully, then you can call the panic macro, which immediately quit the program.
Which crashes:
If we wanted to get a full backtrace of where and in which function did the panic originated, like in this case, we can run with RUST_BACKTRACE=full
environment variable set as:
fn main() {
a();
}
fn a() {
b();
}
fn b() {
c(22);
}
fn c(num: i32) {
if num == 22 {
panic!("Don't pass in 22!");
}
}
which emits,
thread 'main' panicked at 'Don't pass in 22!', src/main.rs:15:9
stack backtrace:
0: rust_begin_unwind
at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/panicking.rs:142:14
2: playground::c
at ./src/main.rs:15:9
3: playground::b
at ./src/main.rs:10:5
4: playground::a
at ./src/main.rs:6:5
5: playground::main
at ./src/main.rs:2:5
6: core::ops::function::FnOnce::call_once
at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/ops/function.rs:248:5
The Result Enum
Errors that are recoverable and can be handled gracefully, in those cases the result is required to be wrapped inside Result
enum:
The Result
enum represent success or failure
For example:
use std::fs::File;
fn main() {
// calling `File::open()` may fail incase file not existing
// and hence it returns a `Result` enum; annotated just for clarity
let f: Result<File, Error> = File::open("hello.txt");
// We must handle both the cases of `Result` using `match`
// shadowing to declare f
let f = match f {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}",error)
};
}
Let's handle it more gracefully by instead of panicking, create a new file:
First bring ErrorKind
struct from io
module into scope, which let us match on the type of errror we get.
and then match on the error we get instead of calling panic!
macro:
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
// which returns an enum representing the kind of error
// creating a new file can also fail, so match expression for that
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e)
},
other_error => {
panic!("Problem opening the file: {:?}", other_error)
}
}
};
This looks not so pretty, so we can rewrite the above code using closures, which we'll learn more about later.
use std::fs::File;
use std::io::ErrorKind;
// `unwrap_or_else()` gives use file or call the anonymous
// function / closure passing in `error`
// inside the closue we have if-else statement;
// - first checking error is created for file not found
// - if yes, then try creating the file which itself can fail
// - in that case, we have another closure binding error and panicking
// - otherwise, for any other error, we just panic
fn main() {
let f = File::open("hello.txt").unwrap_or_else( |error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else( |error| {
panic!("Problem creating the File: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
Useful functions on Result Enum
We can simplify our original code of match
expression and panic
by calling unwrap()
which panics or return the result:
use std::fs::File;
fn main() {
// unwrap does the same thing
// - in success case, return item stored in `Ok` varient
// - otherwise, in `Err` case panic
let f = File::open("hello.txt").unwrap();
}
We can use expect()
method which let us specify the error message or panics.
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open Hello.txt");
}
Error Propagation
Often times, we want to give more control to the caller function of what to do with the error, i.e., we want to return an error instead of handling it ourselves. This is known as Error propagation.
use std::fs::File;
use std::io;
use std::io::Read;
// this function returns a `Result` type returning the file content
// or an `io::Error`
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
// opening file can fail, in which case we return an error
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e)
}
// otherwise we read content in String `s` and return `s`
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e)
}
}
The ? Operator
We can simplify this further, by adding ?
to the call of the open()
method, which gives the unwrapped item otherwise in case of error it returns early with the error:
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
// opening file can fail, in which case we return an error
// let mut f = match f {
// Ok(file) => file,
// Err(e) => return Err(e)
// }
// let mut s = String::new();
// match f.read_to_string(&mut s) {
// Ok(_) => Ok(s),
// Err(e) => Err(e)
// }
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
We can simplify even further by chaining method calls
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
Or maybe even further,
use std::fs::{self, File};
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
What happens if we try to use ?
in our main function?
Info
The main()
can returning Nothing, or a Result
type.
use std::error::Err;
use std::fs::File;
fn main() {
// error!
// error[E0277]: the `?` operator can only be used in a
// function that returns `Result` or `Option` (or another
// type that implements `FromResidual`)
let f = File::open("hello.txt")?;
}
So we can change this to return a Result
type:
use std::error::Err;
use std::fs::File;
// In success case, we return `()` unit
// in error case we return a trait object (any type of error)
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
When to use panic or Result enum
In general, by default you should be using Result
enum and Error propagation. This prevents your program from crashing while the caller function to handle the error as per requirement.
The panic!
should be reserved for exceptional circumstances where recovering from error is not possible.
- Another place where
panic!
is in example code where there is not context on how to deal with error.
You can also use unwrap()
or expect()
in prototype phase code, after which you might want to consider error handling for all such calls.
unwrap()
andexpect()
could also be used in test code, because we want the test to fail if we expected the code to succeed fails to do so.- or use
unwrap()
andexpect()
when you know your call to a function will succeed.