Rust - thiserror

This chapter presents usage of thiserror crate.

Objectives

  • Implement a chain of functions where one calls another.
  • Return an error from the innermost function.
  • Add context to the errors on each subsequent level.
  • Handle the error on the top level by displaying all the causes.
  • Consider std::io::Error as well as user-defined error as a root cause.

Implementation

// config.rs
use std::fmt::Debug;

#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("Failed to read config file")]
    FileReadError(#[from] std::io::Error),
    
    #[error("Failed to parse config file: {0}")]
    ParseConfigError(String),
}

pub fn load_config() -> Result<(), ConfigError> {
    let _text = std::fs::read_to_string("myconfig")?;
    Err(ConfigError::ParseConfigError("Unknown key 'foo'".to_string()))?;
    return Ok(());
}
// setup.rs
use std::fmt::Debug;

use crate::config;

#[derive(thiserror::Error, Debug)]
pub enum SetupError {
    #[error("Failed to configure")]
    LoadConfigError(#[from] config::ConfigError),
}

pub fn setup_app() -> Result<(), SetupError> {
    Ok(config::load_config()?)
}
// launcher.rs
use std::fmt::Debug;
use std::error::Error;

use crate::setup;

#[derive(thiserror::Error)]
pub enum LaunchError {
    #[error("Failed to setup application")]
    SetupAppError(#[from] setup::SetupError),
}

impl Debug for LaunchError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self)?;
        
        let mut err_obj : &dyn Error = self;
        while let Some(source) = err_obj.source() {
            write!(f, "\n  Because: {}", source)?;
            err_obj = source;
        }
        Ok(())
    }
}

pub fn launch_app() -> Result<(), LaunchError> {
    Ok(setup::setup_app()?)
}
// main.rs
mod config;
mod setup;
mod launcher;

fn show_err_stack(top_err: &dyn std::error::Error) {
    eprintln!("{}", top_err);
    #[cfg(debug_assertions)]
    {
        let mut err_obj : &dyn std::error::Error = top_err;
        while let Some(source) = err_obj.source() {
            eprintln!("  Because: {}", source);
            err_obj = source;
        }
    }
}

fn main() {

    if let Err(e) = launcher::launch_app() {
        //println!("{}", e);   // show only top err message
        //println!("{:?}", e); // show all the errors
        show_err_stack(&e);  // show all the errors (customized)
        std::process::exit(1);
    }
   
}

Testing

$ cargo build
$ ./target/debug/thiserror_demo
Failed to setup application
  Because: Failed to configure
  Because: Failed to read config file
  Because: No such file or directory (os error 2)

$ touch myconfig
$ ./target/debug/thiserror_demo
Failed to setup application
  Because: Failed to configure
  Because: Failed to parse config file: Unknown key 'foo'

Open Topics

  • add impl Debug for T for each error that derive(DebugStack)
  • add config file path to FileReadError