Riccardo T.
Riccardo T.

Reputation: 8937

`use std::error::Error` declaration breaks compilation

I wrote a small function to log whole stack traces which looks like this:

fn report_error(error: &Box<dyn std::error::Error>, message: &str) {
    let mut error_msg = Vec::<String>::new();
    error_msg.push(message.to_string());
    if let Some(source) = error.source() {
        error_msg.push("caused by:".into());
        for (i, e) in std::iter::successors(Some(source), |e| e.source()).enumerate() {
            error_msg.push(format!("\t{}: {}", i, e));
        }
    }
    tracing::error!("{}", error_msg.join("\n"));
}

This codes compiles and runs fine with rustc 1.67.1. Compilation fails though as soon as I add a use std::error::Error; declaration at the top, with this error:

   Compiling playground v0.0.1 (/playground)
error: lifetime may not live long enough
 --> src/lib.rs:8:63
  |
8 |         for (i, e) in std::iter::successors(Some(source), |e| e.source()).enumerate() {
  |                                                            -- ^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
  |                                                            ||
  |                                                            |return type of closure is Option<&'2 dyn std::error::Error>
  |                                                            has type `&'1 &dyn std::error::Error`

error: could not compile `playground` due to previous error

I've got no clue about why this happens. It's not like importing Error explicitly changes the declared types in any way here, right? How is this possible?

Here's a link to the Rust Playground demonstrating this code: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=400c01faf36c3ada21d875ce3b6ec0a4

Upvotes: 5

Views: 94

Answers (1)

Peter Hall
Peter Hall

Reputation: 58735

This seems initially very confusing!

Without the import, when you call source() on the Box<dyn Error>, Rust follows a set of rules to find the right method to call, and ends up dereferencing the Box to call the method on its contents.

However, when you import std::error::Error, you bring the trait into scope. This changes things because there is an implementation of Error for &E where E: Error.

The method you want to call (without Error being in scope) effectively has this signature:

fn source<'e>(err: &'e dyn Error) -> Option<&'e (dyn Error + 'static)>

Notice that the lifetime of the reference in the return value is connected to the lifetime of the trait object in the argument. If you chain many calls to source() together, the lifetime of the last one will still be connected to the first.

But the method you are actually calling has this signature:

fn source<'e, 'x>(err: &'e &'x dyn Error) -> Option<&'e (dyn Error + 'static)>

So, when you call source(), the result is not connected to the object in the argument by lifetimes: instead it's connected to a reference to the object. When you use this in a recursive iterator, these lifetimes expire within each iteration.

You can "fix" the issue, by explicitly dereferencing the box so that it is forced to call the method on the inner value:

for (i, e) in std::iter::successors(Some(source), |e| (&**e).source()).enumerate() {
     error_msg.push(format!("\t{}: {}", i, e));
}

This works as it originally did, even if you import std::error::Error, because there is no ambiguity about which implementation of Error we mean.

Alternatively, you could use a loop instead of recursion.

Upvotes: 5

Related Questions