Trait object causing type mismatch

Viewed 430

While implementing a trait from an external library, I found my initial implementation unable to compile.

// From library
trait SomeTrait {
    fn do_thing(&self) -> Result<i32, Box<dyn Error + Sync + Send + 'static>>;
}

// My code
struct Foo;

impl SomeTrait for Foo {
    fn do_thing(&self) -> Result<i32, Box<dyn Error + Sync + Send + 'static>> {
        i32::from_str("42").map_err(|e| Box::new(MyError))
    }
}

#[derive(Debug)]
struct MyError;
impl Display for MyError {...}
impl Error for MyError {}

Error from rustc v1.55.0:

error[E0308]: mismatched types
  --> src/main.rs:17:9
   |
16 |     fn do_thing(&self) -> Result<i32, Box<dyn Error + Sync + Send + 'static>> {
   |                           --------------------------------------------------- expected `Result<i32, Box<(dyn std::error::Error + Send + Sync + 'static)>>` because of return type
17 |         i32::from_str("42").map_err(|e| Box::new(MyError))
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected trait object `dyn std::error::Error`, found struct `MyError`
   |
   = note: expected enum `Result<_, Box<(dyn std::error::Error + Send + Sync + 'static)>>`
              found enum `Result<_, Box<MyError>>`

If I change the body of do_thing to be this, the compiler is happy.

match i32::from_str("42") {
    Ok(v) => Ok(v),
    Err(_) => Err(Box::new(MyError)),
}

I can also do this which the compiler also accepts but is incredibly ugly.

i32::from_str("42").map_err(|e| Box::new(MyError) as Box<dyn Error + Sync + Send + 'static>)

I obviously have a working solution but I'd like to know what the differences between the implementations are to avoid pulling my hair out in the future.

1 Answers

The transformation from Box<MyError> to Box<dyn Error + ...> is known as an unsizing coercion (because it converts from a type with statically known size to one without). The exact rules for which types can be so converted are defined by the special traits CoerceUnsized and Unsize. (These traits are currently unstable, but that just means you are not allowed to implement or directly call them in stable Rust; you can still make use of their effects.)

The documentation tells us the following implementation exists:

impl<T, U, A> CoerceUnsized<Box<U, A>> for Box<T, A>
where
    T: Unsize<U> + ?Sized,
    A: Allocator,
    U: ?Sized, 

This generic implementation covers the case of

impl CoerceUnsized<Box<dyn Error + ...>> for Box<MyError>

so that you can coerce inside map_err, but there is no such implementation for Result, so you cannot perform the coercion on an entire Result, and must arrange for it to happen (implicitly or explicitly) on the error type alone, by bringing the error value out to its own variable of type Box<MyError> which is assigned or returned to a destination that requires Box<dyn Error + ...>.


That's the “why” you asked for. Now a tip for writing the code: you can use as _ to say "I want a coercion, but please infer the type because it's obvious enough”:

    i32::from_str("42").map_err(|e| Box::new(MyError) as _)

This way you don't have to write the lengthy dyn type. You could also define a type alias to reduce repetition while still being explicit:

type DynError = Box<dyn Error + Sync + Send + 'static>;

...

    i32::from_str("42").map_err(|e| Box::new(MyError) as DynError)

However, for the specific case of Box<dyn Error>, an even better option is to just use the ? operator with its automatic Into conversion:

    fn do_thing(&self) -> Result<i32, Box<dyn Error + Sync + Send + 'static>> {
        Ok(i32::from_str("42")?)
    }

This works because ? returns the error with an Into::into(e) conversion, and the standard library contains a generic conversion from any Error to Box<dyn Error + Send + Sync>:

impl<'a, E: Error + Send + Sync + 'a> From<E> for Box<dyn Error + Send + Sync + 'a>

This is probably what the library expected you to use.

In this simple case, the ? is also equivalent to:

    fn do_thing(&self) -> Result<i32, Box<dyn Error + Sync + Send + 'static>> {
        i32::from_str("42").map_err(Into::into)
    }
Related