Using error-stack with Tauri
Tags: tauri, rust
Published: 2023-03-09 21:44:50

# 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:

```rust
#[derive(Debug)]
enum ServiceError {
    Json(serde_json::Error),
    Http(reqwest::Error),
}
```

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

```rust
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"),
        }
    }
}
```

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

```rust
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)
    }
}
```

# Basic error-stack support

The [error-stack](https://crates.io/crates/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:

```rust
#[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{}
```

Then convert the error with `into_report()`:

```rust
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(())
}
```

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)
```

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:

```rust
#[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)
    }
}
```

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

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

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 `{:#}`.