Error Handling

Rust requires you to acknowledge the possibility of an error and take some action before your code will compile. Rust groups errors into two major categories:

  1. recoverable: eg file not found error -> report and retry without stoping the program
    • use Result<T,E> enum with two variants Ok(T) and Err(E)
  2. unrecoverable errors: eg: Runtime failures like failed bounds checks, out of memory, or file I/O errors
    • use panic! to stop execution
    • use Non-panicking APIs (eg: Vec::get) if crashing is not acceptable.

Unrecoverable error with panic!

Rust handles fatal errors (unrecoverable and unexpected) with a "panic". panic can be triggered

  • by taking an action that causes thhe code to panic, usually as symptoms of bugs in program logic.
  • explicitly calling panic! macro

By default, these panics will print a failure message, unwind, clean up the stack, and quit. Unwinding means rust walk back up the stack and cleans up the data from each function it encounters. The unwinding can be caught. We can also abort immediately on panic without cleaning up , which makes the binary as small as possible by adding the following line in Cargo.toml.

[profile.release]
panic = 'abort'
fn main() {
    panic!("crash and burn");
}

Error Message : src/main.rs:2:5 indicates that it’s the second line, fifth character of our src/main.rs file.

In many cases call to panic! might be part of some other function calls in different files, in such cases the filename and line number of panic! might be reported, not the line of code that eventually led to panic! call.

fn main() {
    let v = vec![1, 2, 3];

    v[99];  // accessing invalid index 
}

In other languages, such Buffer overread leads to security vulnerabilities, but Rust will trigger a panic and stop execution. Rust provides a note telling to run with RUST_BACKTRACE to get a backtrace (list of all function call leading to panic) of exactly what happened to cause the error.

$ RUST_BACKTRACE=1 cargo run

Catching the panic


use std::panic;

fn main() {
    let result = panic::catch_unwind(|| "No problem here!");
    println!("{result:?}");

    let result = panic::catch_unwind(|| {
        panic!("oh no!");
    });
    println!("{result:?}");
}

It’s advisable to have your code panic when it’s possible that your code could end up in a bad state (when some assumption, guarantee, contract, or invariant has been unexpectedly broken, such as when invalid values, contradictory values, or missing values are passed to your code). Attempting to operate on invalid data (attempt to access an out-of-bounds memory, violation of function contracts) can expose your code to vulnerabilities. Such violations and panic should be explained in API documentations.

Recoverable error with Result<T,E>

Many functions in rust returns a Result to state if it succeeded or failed in its operation.

fn divide(a:f64, b:f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Cannot divide by zero"))
    } else {
        Ok(a / b)
    }
}

While reading a file, the file might not exist, or we might not have permission to access the file, leading to different types of errors, Result enum can convey such information.


use std::fs::File;       // T for file open
use std::io::ErrorKind;  // E variants

fn main() {
    let greeting_file_result = File::open("hello.txt");  // returns a result

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() { 
            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:?}");
            }
        },
    };
}

Match can be verbose, and might not communicate intent well. Helper methods defined on Result can be helpful for more specific task. Such as - .unwrap(): return value inside Ok or panic! if Err.

  • .expect(Msg): allows to return Msg if Err, else return value inside Ok.
  • unwrap_or_else: takes a closure as argument and runs it if the Result is an Err.
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = 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:?}");
        }
    });
}

The above methods causes the code to panic. When code panics, there’s no way to recover. A called function instead of handling errors within itself can return (propagate) the error to the calling function to handle it. The calling code may have better context and it could choose to attempt to recover in a way that’s appropriate for its situation, or it could decide that an Err value in this case is unrecoverable, so it can call panic! and turn your recoverable error into an unrecoverable one. However, using unwrap, expect can be useful in cases where it’s logically impossible to get the Err value such as while parsing the hardcoded text.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),  // propagate the error
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),  // propagate the error, returns the last expression
    }
}

This pattern of propagating errors is so common in Rust that Rust provides the question mark operator ? to make this easier. ? returns the value inside Ok or propagate the error, we can further chain method calls with ? to make shorter expressions.

? calls the from function on the error type returned by the expression, converting it into a Result<T, E> type of the current function. This is useful when a function returns one error type to represent all the ways a function might fail, even if parts might fail for many different reasons.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

// shorter and more ergonomic way to directly read string from file
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

The ? operator can only be used in functions whose return type is a Result, i.e. the main function should return a Result<T,E> or Option<T> returning None or Some.