Search code examples
rusterror-handlingthiserror

Rust error handling - why does this give different output?


I'm having trouble getting miette to give consistent output.

I'd expect the program below to give the same output whether I pass in "good" (which gives the fancy formatting I want) or "bad" (which prints a more debug-like error message), but one way I get things pretty-printed and one I get a much more basic message and I can't figure out why - what's going on?

(This is heavily copied from the this example - note there's on dbg!() statement at the start, it's the Error: ... output at the end that I expect to be different.)

use std::error::Error;

use miette::{Diagnostic, SourceSpan};
use miette::{NamedSource, Result as MietteResult};
use thiserror::Error;

#[derive(Error, Debug, Diagnostic)]
#[error("oops!")]
#[diagnostic(
    code(oops::my::bad),
    url(docsrs),
    help("try doing it better next time?")
)]
struct MyBad {
    // The Source that we're gonna be printing snippets out of.
    // This can be a String if you don't have or care about file names.
    #[source_code]
    src: NamedSource,
    // Snippets and highlights can be included in the diagnostic!
    #[label("This bit here")]
    bad_bit: SourceSpan,
}

fn this_gives_correct_formatting() -> MietteResult<()> {
    let res: Result<(), MyBad> = Err(MyBad {
        src: NamedSource::new("bad_file.rs", "source\n  text\n    here".to_string()),
        bad_bit: (9, 4).into(),
    });

    res?;

    Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
    if std::env::args().nth(1).unwrap() == "bad" {
        let res: Result<(), MyBad> = Err(MyBad {
            src: NamedSource::new("bad_file.rs", "source\n  text\n    here".to_string()),
            bad_bit: (9, 4).into(),
        });

        dbg!(&res);

        res?;

        Ok(())
    } else if std::env::args().nth(1).unwrap() == "good" {
        let res = this_gives_correct_formatting();

        dbg!(&res);

        res?;

        Ok(())
    } else {
        panic!("Pass either 'good' or 'bad'");
    }
}

In my Cargo.toml:

[dependencies]
miette = { version = "5.5.0", features = ["fancy"] }
thiserror = "1.0.39"

Output from a session:

$ cargo run good
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/rust-play good`
[src/main.rs:50] &res = Err(
    MyBad {
        src: NamedSource {
            name: "bad_file.rs",
            source: "<redacted>",
        ,
        bad_bit: SourceSpan {
            offset: SourceOffset(
                9,
            ),
            length: SourceOffset(
                4,
            ),
        },
    },
)
Error: oops::my::bad (https://docs.rs/rust-play/0.1.0/rust_play/struct.MyBad.html)

  × oops!
   ╭─[bad_file.rs:1:1]
 1 │ source
 2 │   text
   ·   ──┬─
   ·     ╰── This bit here
 3 │     here
   ╰────
  help: try doing it better next time?

$ cargo run bad
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/rust-play bad`
[src/main.rs:42] &res = Err(
    MyBad {
        src: NamedSource {
            name: "bad_file.rs",
            source: "<redacted>",
        ,
        bad_bit: SourceSpan {
            offset: SourceOffset(
                9,
            ),
            length: SourceOffset(
                4,
            ),
        },
    },
)
Error: MyBad { src: NamedSource { name: "bad_file.rs", source: "<redacted>", bad_bit: SourceSpan { offset: SourceOffset(9), length: SourceOffset(4) } }

Solution

  • The "good" case produces an error of type miette::Error while the "bad" case produces an error of type MyBad. Presumably the former type has a Display implementation that produces the fancy human-readable output.

    Note that the ? operator doesn't just return the error in the Err case, it also attempts to convert it using Into::into(). x? is mostly equivalent to:

    match x {
        Ok(v) => v,
        Err(e) => return Err(e.into()),
    }
    

    So if x has type Result<_, E>, the function is declared with the return type Result<_, F>, and there is an implementation Into<F> for E, the ? operator will transparently do this conversion. This is easy to miss, so it's understandable that you didn't catch it.

    If you replace the return type of main() with MietteResult<()>, you should get a compile-time error that the return types of the two conditional blocks don't match.

    You can fix this by converting the error value in the "bad" case to miette::Error:

    let res: Result<(), miette::Error> = Err(MyBad { ... }.into());