Error Handling Isn't All About Errors

Jane Lusby

@yaahc_ / yaah.dev

Slide Template by Rebecca Turner @16kbps

About Me

Awesome Rust Mentors

Why Error Handling?

eyre

color-eyre

 cargo run --example usage
Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:32
   1: usage::read_config
      at examples/usage.rs:38

Suggestion: try using a file that exists next time

Backtrace omitted.
Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
 cargo run --example usage
Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:32
   1: usage::read_config
      at examples/usage.rs:38

Suggestion: try using a file that exists next time

Backtrace omitted.
Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
 cargo run --example usage
Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:32
   1: usage::read_config
      at examples/usage.rs:38

Suggestion: try using a file that exists next time

Backtrace omitted.
Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
 cargo run --example usage
Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:32
   1: usage::read_config
      at examples/usage.rs:38

Suggestion: try using a file that exists next time

Backtrace omitted.
Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
 cargo run --example usage
Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:32
   1: usage::read_config
      at examples/usage.rs:38

Suggestion: try using a file that exists next time

Backtrace omitted.
Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
 RUST_BACKTRACE=1 cargo run --example usage
// ...
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━
                         ⋮ 5 frames hidden ⋮
   6: usage::read_file::h10b2389c2b814452
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:35
   7: usage::read_config::hf7150b146edb25d9
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:40
   8: usage::main::hc3df11a6ea0d044d
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:11
                        ⋮ 10 frames hidden ⋮
// ...
Run with RUST_BACKTRACE=full to include source snippets.
 RUST_BACKTRACE=1 cargo run --example usage
// ...
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━
                         ⋮ 5 frames hidden ⋮                       
   6: usage::read_file::h10b2389c2b814452
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:35
   7: usage::read_config::hf7150b146edb25d9
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:40
   8: usage::main::hc3df11a6ea0d044d
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:11
                        ⋮ 10 frames hidden ⋮                       
// ...
Run with RUST_BACKTRACE=full to include source snippets.
 RUST_BACKTRACE=1 cargo run --example custom_filter
// ...
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━
                         ⋮ 5 frames hidden ⋮                       
   6: custom_filter::read_file::h0afee8fe0960bf02
      at /home/jlusby/git/yaahc/color-eyre/examples/custom_filter.rs:53
   7: custom_filter::read_config::h6622065848c69b29
      at /home/jlusby/git/yaahc/color-eyre/examples/custom_filter.rs:58
                        ⋮ 11 frames hidden ⋮                       
// ...
Run with RUST_BACKTRACE=full to include source snippets.
 RUST_BACKTRACE=1 cargo run --example panic_hook --no-default-features
The application panicked (crashed).
Message:  No such file or directory (os error 2)
Location: examples/panic_hook.rs:37

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━
                        ⋮ 13 frames hidden ⋮                       
  14: panic_hook::read_file::h1a2c1d2710c16ca9
      at /home/jlusby/git/yaahc/color-eyre/examples/panic_hook.rs:37
  15: panic_hook::read_config::h2751dcca3305a9a3
      at /home/jlusby/git/yaahc/color-eyre/examples/panic_hook.rs:43
                        ⋮ 11 frames hidden ⋮                       

Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering.
Run with RUST_BACKTRACE=full to include source snippets.
 cargo run --example custom_section
Error:
   0: the cat could not be got
   1: cmd exited unsuccessfully

Command:
   "git" "cat"

Stderr:
   git: 'cat' is not a git command. See 'git --help'.

   The most similar commands are
   	clean
   	mktag
   	stage
   	stash
   	tag
   	var

Suggestion: Maybe that isn't what git is for...
 cargo run --example multiple_errors
Error:
   0: encountered multiple errors

Error:
   0: The task could not be completed
   1: The task you ran encountered an error

Error:
   0: The machine is unreachable
   1: The machine you're connecting to is actively on fire

Error:
   0: The file could not be parsed
   1: The file you're parsing is literally written in c++ instead of rust, what the hell

What Is Error Handling?

  • Annoying
  • Defining errors
  • Propagating errors and gathering context
  • Reacting to specific errors
  • Discarding errors
  • Reporting errors and gathered context

Recoverable
vs
Non-Recoverable

Panic

// if the index is past the end of the slice
} else if self.end > slice.len() {
    panic!(
        "index {} out of range for slice of length {}",
        self.end,
        slice.len()
    );
}

Panic

// if the index is past the end of the slice
} else if self.end > slice.len() {
    panic!(
        "index {} out of range for slice of length {}",
        self.end,
        slice.len()
    );
}

Result

#[must_use]
enum Result<T, E> {
    /// Contains the success value
    Ok(T),
    /// Contains the error value
    Err(E),
}

Result

match result {
    Ok(success) => println!("we got the value {}!", success),
    Err(error) => println!("uh oh we got an error: {}", error),
}

Try and ?

Try and ?

let config = match get_config() {
    Ok(success_value) => success_value,
    Err(error_value) => return Err(Error::from(error_value)),
};

// vs

let config = get_config()?;

The Error Trait

  • Representing an open set of errors
  • Reacting to specific errors in an open set
  • Reporting Interface for all errors
Recoverable
  • Defining
    • types and traits
  • Propagating
    • ?
  • Matching and Reacting
    • match or downcast
  • Discarding
    • drop or unwrap
  • Reporting
    • Error Trait
Non-Recoverable
  • Defining
    • panic!
  • Propagating
    • builtin
  • Matching and Reacting
    • pls don’t
  • Discarding
    • catch_unwind
  • Reporting
    • panic hook

Definitions

  • Error: A description of why an operation failed
  • Context: Any information relevant to an error or an error report that is not itself an error
  • Error Report: Printed representation of an error and all of its associated context

The Error Trait

pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }

    fn backtrace(&self) -> Option<&Backtrace> {
        None
    }
}

The Error Trait - An Error

#[derive(Debug)]
struct DeserializeError;

impl std::fmt::Display for DeserializeError {
    fn fmt(
        &self,
        f: &mut std::fmt::Formatter<'_>,
    ) -> std::fmt::Result {
        write!(f, "unable to deserialize type")
    }
}

impl std::error::Error for DeserializeError {}

The Error Trait - A Reporter

fn report(error: &(dyn std::error::Error + 'static)) {
    print!("Error:");

    let errors =
        std::iter::successors(Some(error), |e| e.source());

    for (ind, error) in errors.enumerate() {
        print!("\n   {}: {}", ind, error);
    }

    if let Some(backtrace) = error.backtrace() {
        print!("\n\nBacktrace: {}", backtrace);
    }
}

The Error Trait

trait GoError {
    fn msg(&self) -> String;
}

trait CppError {
    fn msg(&self) -> &'spooky str;
}

The Error Trait

ERROR read_config:read_file{path="fake_file"}: Error: Unable
to read config: No such file or directory (os error 2)

// vs

Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:52
   1: usage::read_config
      at examples/usage.rs:58

The Error Trait

ERROR read_config:read_file{path="fake_file"}: Error: Unable
to read config: No such file or directory (os error 2)

// vs

Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:52
   1: usage::read_config
      at examples/usage.rs:58

The Error Trait

ERROR read_config:read_file{path="fake_file"}: Error: Unable
to read config: No such file or directory (os error 2)

// vs

Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:52
   1: usage::read_config
      at examples/usage.rs:58

The Error Trait is restrictive

  • Can only represent errors with a single source
  • Can only access 3 forms of context

Error Return Traces

Error:
    0: ERROR MESSAGE
        at LOCATION
    1: SOURCE'S ERROR MESSAGE
        at SOURCE'S LOCATION
    2: SOURCE'S SOURCE'S ERROR MESSAGE
    ...
trait CommandExt {
    fn output2(&mut self) -> Result<String, eyre::Report>;
}

impl CommandExt for std::process::Command {
    fn output2(&mut self) -> Result<String, eyre::Report> {
        let output = self.output()?;

        let stdout = String::from_utf8_lossy(&output.stdout)
            .into_owned();

        if output.status.success() {
            Ok(stdout)
        } else {
            Err(eyre!("command exited unsuccessfully"))
        }
    }
}
 cargo run
Error:
   0: command exited unsuccessfully
impl CommandExt for std::process::Command {
    fn output2(&mut self) -> Result<String, eyre::Report> {
        let output = self.output()?;

        let stdout = String::from_utf8_lossy(&output.stdout).into_owned();

        if output.status.success() {
            Ok(stdout)
        } else {
            let cmd = format!("{:?}", self);

            Err(eyre!("command exited unsuccessfully"))
                .section(cmd.header("Command:"))
        }
    }
}
 cargo run
Error:
   0: command exited unsuccessfully

Command:
   "git" "cat"
fn main() -> Result<(), eyre::Report> {
    color_eyre::install()?;

    let _ = std::process::Command::new("git")
        .arg("cat")
        .output2()?;


    Ok(())
}
fn main() -> Result<(), eyre::Report> {
    color_eyre::install()?;

    let _ = std::process::Command::new("git")
        .arg("cat")
        .output2()
        .wrap_err("the cat could not be got")?;

    Ok(())
}
 cargo run
Error:
   0: the cat could not be got
   1: command exited unsuccessfully

Command:
   "git" "cat"

        let stdout = String...

        if output.status.success() {
            Ok(stdout)
        } else {
            let cmd = format!("{:?}", self);
            let stderr =
                String::from_utf8_lossy(&output.stderr)
                    .into_owned();

            Err(eyre!("command exited unsuccessfully"))
                .section(cmd.header("Command:"))
                .section(stdout.header("Stdout:"))
                .section(stderr.header("Stderr:"))
        }
    }
}
 cargo run
Error:
   0: the cat could not be got
   1: command exited unsuccessfully

Command:
   "git" "cat"

Stderr:
   git: 'cat' is not a git command. See 'git --help'.

   The most similar commands are
   	clean
   	mktag
   	stage
   	stash
   	tag
   	var

TIPS - Reporters

  • Reporters almost always impl From<E: Error>
  • if they do they cannot impl Error
    • anyhow::Error
    • eyre::Report
    • Box<dyn Error>
  • don't compose well

Libraries

  • Defining
  • Propagating and Gathering Context
  • Matching and Reacting
  • Discarding
  • Reporting

Defining - thiserror

#[derive(Debug)]
pub enum DataStoreError {

    Disconnect(io::Error),

    Redaction(String),

    InvalidHeader {
        expected: String,
        found: String,
    },
}

Defining - thiserror

#[derive(Debug, thiserror::Error)]
pub enum DataStoreError {

    Disconnect(io::Error),

    Redaction(String),

    InvalidHeader {
        expected: String,
        found: String,
    },
}

Defining - thiserror

#[derive(Debug, thiserror::Error)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
}

Defining - thiserror

#[derive(Debug, thiserror::Error)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[source] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
}

Defining - thiserror

#[derive(Debug, thiserror::Error)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
}

Defining - displaydoc

#[derive(Debug, thiserror::Error, displaydoc::Display)]
pub enum DataStoreError {
    /// data store disconnected
    Disconnect(#[from] io::Error),
    /// the data for key `{0}` is not available
    Redaction(String),
    /// invalid header (expected {expected:?}, found {found:?})
    InvalidHeader {
        expected: String,
        found: String,
    },
}

Defining - SNAFU

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Unable to read configuration from {}: {}", path.display(), source))]
    ReadConfiguration { source: io::Error, path: PathBuf },
}

fn process_data() -> Result<(), Error> {
    let path = "config.toml";
    let configuration = fs::read_to_string(path)
        // wrap error while capturing `path` as context
        .context(ReadConfiguration { path })?;
    Ok(())
}

Defining - anyhow/eyre

// Construct an ad-hoc error
Err(eyre!("file not found"))?

// Constructing an ad-hoc wrapping error
fallible_fn()
    .wrap_err("failed operation")?;

TIPS - Defining

  • API stability
  • Stack Size

API Stability


#[derive(Debug, Display, Error)]
pub enum Error {
    /// Could not deserialize type

    Deserialize{
        source: io::Error,
    },
}

API Stability

#[non_exhaustive]
#[derive(Debug, Display, Error)]
pub enum Error {
    /// Could not deserialize type

    Deserialize{
        source: io::Error,
    },
}

API Stability

#[non_exhaustive]
#[derive(Debug, Display, Error)]
pub enum Error {
    /// Could not deserialize type
    #[non_exhaustive]
    Deserialize{
        source: io::Error,
    },
}

Stack Size

struct LargeError { ... }

///     /!\ Doesn't impl Error /!\
fn fallible_fn() -> Result<(), Box<dyn Error>> {
    // ...
}

///         Does impl Error !!! \o/
fn fallible_fn() -> Result<(), Box<LargeError>> {
    // ...
}

Propagating - fehler

#[fehler::throws(i32)]
fn foo(x: bool) -> i32 {
    if x {
        0
    } else {
        fehler::throw!(1);
    }
}

Gathering Context - tracing-error

// instrument the error
let error = std::fs::read_to_string("myfile.txt")
    .in_current_span();

// extract it from `dyn Error`
let error: &(dyn std::error::Error + 'static) = &error;
assert!(error.span_trace().is_some());

Gathering Context - extracterr

#[derive(Debug, Display, Error)]
struct ExampleError;

type Error =
    extracter::Bundled<ExampleError, backtrace::Backtrace>;

let error = Error::from(ExampleError);

// extract it from `dyn Error`
let error: &(dyn std::error::Error + 'static) = &error;
assert!(error.extract::<backtrace::Backtrace>().is_some());

Matching and Reacting - anyhow/eyre

#[derive(Display)]
/// Random error message
struct FooMessage;

let report = fallible_fn()
    .wrap_err(FooMessage)
    .unwrap_err();

assert!(report.downcast_ref::<FooMessage>().is_ok());

Discarding

  • Nothing

Reporting

  • Reporters: anyhow/eyre
  • Hooks: color-eyre, stable-eyre, jane-eyre, color-anyhow (soon)

Library vs Application

  • Library => error defining libraries or by hand
  • Application => adhoc error defining + error reporting libraries

Fin