Using error-stack with Tauri

randomengy

David Rickard

Posted on March 9, 2023

Using error-stack with Tauri

The problem

Rust's built-in error handling is rather bare-bones and requires a whole lot of effort to preserve a proper stack trace, where other languages give you that for free.

In order to show the inner error along with your error, you need to wrap the inner error explicitly:

#[derive(Debug)]
enum ServiceError {
    Json(serde_json::Error),
    Http(reqwest::Error),
}
Enter fullscreen mode Exit fullscreen mode

You then need to implement Display with case arms for each enum:

impl fmt::Display for ServiceError{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ServiceError::Json(..) =>
                write!(f, "Could not parse the JSON"),
            ServiceError::Http(..) =>
                write!(f, "The HTTP call failed"),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And then implement From to allow the inner error types to convert to your error:

impl From<serde_json::Error> for ServiceError {
    fn from(err: serde_json::Error) -> ServiceError {
        ServiceError::Json(err)
    }
}

impl From<reqwest::Error> for ServiceError {
    fn from(err: reqwest::Error) -> ServiceError {
        ServiceError::Http(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Basic error-stack support

The error-stack crate makes this a lot better. Its error is a Report, which stores the chain of errors it's encountered. That means you can define an error like this:

#[derive(Debug)]
struct ServiceError;

impl fmt::Display for ServiceError{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str("There was an error calling the service.")
    }
}

impl Error for ServiceError{}
Enter fullscreen mode Exit fullscreen mode

Then convert the error with into_report():

use error_stack::{Result, IntoReport};

fn parse_it(value: &str) -> Result<(), ServiceError> {
    let parsed: serde_json::Value = serde_json::from_str(value)
        .into_report()
        .change_context(ServiceError)?;
    println!("We parsed: {}", parsed);

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

The into_report() packages up an outside error into a Result, and change_context() adds our own error type as another link in the error chain. You can add another link by calling change_context() again with a different error type. No more annoying From implemenations!

The Report prints out beautifully with all information:

Error: experiment error: could not run experiment
├╴at examples/demo.rs:51:18
├╴unable to set up experiments
│
├─▶ invalid experiment description
│   ├╴at examples/demo.rs:21:10
│   ╰╴experiment 2 could not be parsed
│
╰─▶ invalid digit found in string
    ├╴at examples/demo.rs:19:10
    ├╴backtrace with 31 frames (1)
    ╰╴"3o" could not be parsed as experiment

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

backtrace no. 1
   0: std::backtrace_rs::backtrace::libunwind::trace
             at /rustc/e972bc8083d5228536dfd42913c8778b6bb04c8e/library/std/src/../../backtrace/src/backtrace/libunwind.rs:93:5
   1: std::backtrace_rs::backtrace::trace_unsynchronized
             at /rustc/e972bc8083d5228536dfd42913c8778b6bb04c8e/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
   2: std::backtrace::Backtrace::create
             at /rustc/e972bc8083d5228536dfd42913c8778b6bb04c8e/library/std/src/backtrace.rs:332:13
   3: core::ops::function::FnOnce::call_once
             at /rustc/e972bc8083d5228536dfd42913c8778b6bb04c8e/library/core/src/ops/function.rs:250:5
   4: core::bool::<impl bool>::then
             at /rustc/e972bc8083d5228536dfd42913c8778b6bb04c8e/library/core/src/bool.rs:71:24
   5: error_stack::report::Report<C>::from_frame
             at ./src/report.rs:288:25
   6: error_stack::report::Report<C>::new
             at ./src/report.rs:274:9
   7: error_stack::context::<impl core::convert::From<C> for error_stack::report::Report<C>>::from
             at ./src/context.rs:83:9
   8: <core::result::Result<T,E> as error_stack::result::IntoReport>::into_report
             at ./src/result.rs:203:31
   (For this example: additional frames have been removed)
Enter fullscreen mode Exit fullscreen mode

You can attach other bits of arbitrary data to the report as well.

Tauri integration

Tauri will not let you directly return a error_stack::Result from a #[tauri::command]. If you try, it will complain:

no method blocking_kind on type &Result<(), Report<ServiceError>>

Tauri lets you return anything that implements Serialize, but a Report does not. To get around this we can use a wrapper:

#[derive(Debug, Serialize)]
pub struct CommandError(String);

impl<C> From<Report<C>> for CommandError {
    fn from(err: Report<C>) -> CommandError {
        CommandError(format!("{:?}", err))
    }
}

impl fmt::Display for CommandError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(&self.0)
    }
}
Enter fullscreen mode Exit fullscreen mode

That allows us to easily map our Reports to an error type that Tauri can send to the frontend:

#[tauri::command]
async fn call_the_service() -> core::result::Result<(), CommandError> {
    my_module::call_service().await?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

That will be visible in the Dev console for the frontend page. {:?} is the debug format that includes all error information; if you want to obfuscate for production, you'll want to use {:#}.

💖 💪 🙅 🚩
randomengy
David Rickard

Posted on March 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related