Rust error propagation with distinct type across propagation levels

Viewed 57

What is the recommended way of propagating different error types in Rust (especialy when their definition doesn't merge)?

The Scenario

Initially, I was curious about how to stop a running thread in Rust. Search suggests it goes through channel communication + conditional.

So, I came up with this code which (1) should loop 5 iterations, (2) with 500 ms sleep in-between iterations, and (3) will stop right after the third iteration.

use std::any::Any;
use std::sync::mpsc;
use std::sync::mpsc::SendError;
use std::thread;
use std::time::Duration;

fn main() -> Result<(), Box<dyn Any + Send>> {
    fn some_condition(i: i32) -> bool { 3 == i }

    thread::spawn(|| -> Result<(), SendError<bool>> {
        let (tx, rx) = mpsc::channel::<bool>();

        for i in 0..5 {
            if some_condition(i) { tx.send(true)? }
            if let Ok(true) = rx.try_recv() { break }

            println!("i = {}", i);
            thread::sleep(Duration::from_millis(500));
        }

        Ok(())
    }).join()?; // Result<(), SendError<bool>>
                // I get away with a warning: which the expected compiler behaviour
    Ok(())
}

The Problem

While this code run with the expected behaviour, I have a warning for not using the result resulting from the ? error propagation.

So I was tempted to do ?? instead of just ?. But then, I got the following compiler warning error:

error[E0277]: `?` couldn't convert the error to `Box<dyn Any + Send>`
  --> src/main.rs:44:53
   |
27 | fn main() -> Result<(), Box<dyn Any + Send>> {
   |              ------------------------------- expected `Box<dyn Any + Send>` because of this
...
44 |     }).join()/*.map_err(|error| Reference(error))*/??;
   |                                                     ^ the trait `From<SendError<bool>>` is not implemented for `Box<dyn Any + Send>`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
   = help: the following implementations were found:
             <Box<(dyn std::error::Error + 'a)> as From<E>>
             <Box<(dyn std::error::Error + 'static)> as From<&str>>
             <Box<(dyn std::error::Error + 'static)> as From<Cow<'a, str>>>
             <Box<(dyn std::error::Error + 'static)> as From<String>>
           and 22 others
   = note: required because of the requirements on the impl of `FromResidual<Result<Infallible, SendError<bool>>>` for `Result<(), Box<dyn Any + Send>>`

Then I realised it is because Box<dyn Any + Send> and From<SendError<bool>> are not overlapping types... And, the first propagation match the former type and the second, the latter.

So I suspect from here that the problem could be phrased as: [H]ow to define overlapping types for such use cases?

Envisioned Solution

Intuitively, I tried adding this code that fails (as expected):

// Only traits defined in the current crate can be implemented for arbitrary types [E0117]
impl From<SendError<bool>> for Box<dyn Any + Send> {
    fn from(_: SendError<bool>) -> Self {
        todo!()
    }
}

However, wrapping errors under the same type solves the issue.

Adding:

use crate::Error::{Reference, Source};

#[derive(Debug)]
enum Error {
    Reference(Box<dyn Any + Send>),
    Source(SendError<bool>)
}

impl From<Error> for SendError<bool> {
    fn from(_: Error) -> Self {
        todo!()
    }
}

impl From<SendError<bool>> for Error {
    fn from(_: SendError<bool>) -> Self {
        todo!()
    }
}

Then, changing lines to match

// ...
                tx.send(true).map_err(|error| Source(error))?;
// ...
    }).join().map_err(|error| Reference(error))??;

Bottom Line

It works but is so verbose that I's like it for Rust to have another way (something in the likes of Infallible, maybe).

What is the recommended way of propagating different error types in Rust (especialy when their definition doesn't merge)?

1 Answers

The idiomatic way depends on whether you're writing a library or an application.

For applications, you usually use some dyn Trait as you just need to display the error.

Usually you don't use dyn Any for errors in Rust but dyn std::error::Error which is a trait for... well, errors. It contains things like (optional, unstable currently) backtrace, error chain and description via Display. Or sometimes you use a crate like anyhow, which is basically a better wrapper for std::error::Error.

The problem is that join() returns std::thread::Result that has an error type Box<dyn Any + Send>. This is because it does not represent "just" error, but a panic (that possibly occurred in the spawned thread) - and panic payload is always Box<dyn Error + Send>. Usually you just unwrap() this to propagate the panic to the spawning thread, but if you want to convert it to a dyn Error, you cannot since there's no way to convert dyn Any to dyn Error.

The way I would tackle this is by creating a wrapper, PanicError, that wraps Box<dyn Error> and implements std::error::Error:

#[derive(Debug)]
pub struct PanicError {
    pub payload: Box<dyn Any + Send>,
}

impl From<Box<dyn Any + Send>> for PanicError {
    fn from(payload: Box<dyn Any + Send>) -> Self {
        Self { payload }
    }
}

impl fmt::Display for PanicError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let msg = match self.payload.downcast_ref::<&'static str>() {
            Some(&msg) => msg,
            None => match self.payload.downcast_ref::<String>() {
                Some(msg) => msg,
                None => "Box<dyn Any>",
            },
        };
        f.pad(msg)
    }
}

impl Error for PanicError {}

Then you can use it like:

    .join()
    .map_err(PanicError::from)??;

For libraries, you want to give maximum flexibility to the consumers. They should be able to print the error, but also to match the specific type of error occurred. The usual way to do that is by creating an enum with variant for each possible error, a From implementation from each internal error to its variant (so you can use ? to propagate errors), and Display and Error implementations. You don't need to write this by hand: there are crates such as thiserror that will help you. I would still use the PanicError wrapper, though, as it is easier to work with:

#[derive(Debug, thiserror::Error)]
pub enum MyError {
    #[error("a thread panicked: {0}")]
    Panic(
        #[source]
        #[from]
        PanicError,
    ),
    #[error("failed to send close signal")]
    CloseSignalSendingFailure(
        #[source]
        #[from]
        SendError<bool>,
    ),
}

fn main() -> Result<(), MyError> { ... }
Related