Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 7: Error Handling

Panic:

  • Out-of-bounds array access
  • Integer division by zero
  • Calling .expect() on a Result that happens to be Err
  • Assertion failure

panic() accetps optional println() style arguments, for building an error message.

IMPORTANT: When a panic happens, by default:

  1. Stack is unwound: Any temoporary values, local variable, or arguments that the current function was using are dropped, in the reverse of the order they were created. Dropping a value simply means cleaning up after it: any Strings or Vecs the program wwas using are freed, any open Files are closed, an so on. User-defined drop methods are called too. ** In the particular case of pirate_share, there's nothing to clean up.** Once the current function call is cleaned up, we move on to its caller, dropping is variables and arguments the same way. Then we move up to that function's caller, and so on up the stack.
  2. Finally, the thread exits. It the panicking thread was the main thread, then the whole process exits (with a non-zero code).

Panic is safe. It doesn't violate any of Rust's safety rulse: even if you manage to panic in the middle of standard library method, it will ne ver leave a dangling pointer or a hald-initialized value in memory!

Panic is per thread. A parent thread can find out when a child thread panics and handle the error gracefully!

There is also a way to catch stack unwinding, allowing the thread to survive and continue running. The standard library function std::panic::catch_unwind() does this.

You can use threads and catch_unwind() to handle panic, making your program more robust. One imporatant caveat is that these tools only catch panics that unwind the stack. Not every panic proceeds this way!

If a drop() method triggers a second panic while Rust is still trying to clean up after the first,this is considered fatal. Rust stops unwinding and aborts the whole process.

Also, Rust's panic behavior is customizable. If you compile with -c panic=abort, the first panic in your program immediately aborts the process. (With this option, Rust does not need to know how to unwind the stack, so this can reduce the size of your compiled code).

How to handle a Result: match is a bit verbose. So Result<T, E> offers a variety of methods:

  • result.is_ok(), result.is_err()
  • result.ok() Returns the success value, if any, as an Option<T>
  • result.err() Return the value, if any, as an Option<E>
  • result.unwrap_or(fallback) Returns the success value, if result is a success result. Otherwise, it returns fallback, discarding the error.
  • result.unwrap_or_else(fallback_fn) This is the same, but instead of passing a fallback value directly, you pass a function or closure. Note: This is for cases where it would be wasteful to compute a fallback value if you're not going to use it. The fallback_fn is called only if we have an error result.
  • result.unwrap() Also returns the success value, if result is success. However, if result is an error result, this method panics.
  • result.expect(message) This is the same as unwrap(), but provides a message that it prints in case of panic.
  • result.as_ref() Converts a Result<T, E> to Result<&T, &E>.
  • result.as_mut() Convers a Result<T, E> to Result<&mut T, &mut E>

Note: One reason these two methods are usefull is that all of the other methods listed here, except .is_ok() and .is_err(), consumes the result they operate on.

Result Type Aliases

Sometimes you'll see in Rust documentation that seems to omit the error type of Result:

#![allow(unused)]
fn main() {
fn remove_file(path: &Path) -> Result<()>
}

This means that a Result type alias is being used. Modules often define a Result type alias to avoid having to repeat an error ttype that's used consistenly by almost every function in the module. For example the standard library's std::io module includes this line of code:

#![allow(unused)]
fn main() {
pub type Result<T> = result::Result<T, Error>;
}

Printing an error value does not also print out its source. If you want to be suire to print all the available information:

#![allow(unused)]
fn main() {
use std::error:Error;
use std::io::{Write, stderror};

fn print_error(mut err: &dyn Error) {
    let _ = writeln!(stderr(), "error: {}", err);
    while let Some(source) = err.source() {
        let _ = writeln!(stderr(), "caused by: {}", source);
        err = source;
    }
}
}
  • Note that writeln! macro works like println!, except that it writes to a stream of your choice.
  • We could use the eprintln! macro to do the same thing, but eprintln! panics if an error occurs.

Propagating Errors

It is simply too much code to use a 10-line match statement every place where something might go wrong.

#![allow(unused)]
fn main() {
let weather = get_weather(hometown)?;
}

The behvaior of ? operator depends on whether this function returns a success result or an error result:

  • On success, it unwraps the Result to get the success value inside.
  • On error, it immediately returns from the enclosing function, passing the error result up the call chain. To ensure that this works, ? can only be used on a Result in functions that have a Result return type.

? also works similarly with the Option type. In a function that returns Option, you can use ? to unwrap a value and return early in the case of None.

Working with Multiple Error ttype

#![allow(unused)]
fn main() {
use std::io::{Self, BufRead};

fn read_numers(file: &mut dyn BufRead) -> Result<Vec<i64>, io:Error> {
    let mut numbers = vec![];
    for line_result in file.lines() {
        let line = line_result?;
        numbers.push(line.parse()?);
    }
    Ok(numbers)
}
}

Here the compiler will complain that it ? cannot convert a std::num::ParseIntError value to the type std::io:Error.

What should we do then?

  • Type the thiserror crate, which is designed to heop you define good error types with just a few lines of code.
  • A simpler approach is to use what's build into Rust. All of the standard library error types can be converted to the type Box<dyn std::error::Error + Send + Sync + 'static>. This might seem mouthful. However, dyn std::error:Error represents "any error" and Send + Sync + 'static makes it safe to pass between threads, which you'll often want.
type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;
  • You could also consider using crate anyhow

If you're calling a function that returns a GenericResult and you want to handle one particular kind of error but let all other propagate out, use the generic method error.downcast_ref::<ErrorType>(). It borrows a reference to the error, if it happens to be the particular type of error you're looking for.

#![allow(unused)]
fn main() {
loop {
    match compile_project(){ 
        Ok(()) => return Ok(()),
        Err(err) => {
            if let Some(mse) = err.downcast_ref::<MissingSemicolonError>() {
                insert_semicolon_in_source_code(mse.file(), mse.line())?;
                continue;
            }
            return Err(err)
        }
    }
}
}