David Rickard
Posted on March 9, 2023
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),
}
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"),
}
}
}
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)
}
}
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{}
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(())
}
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:
#[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:
#[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 {:#}
.
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
August 14, 2024