Rust error handling - why does this give different output?

241 views Asked by At

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) } }
1

There are 1 answers

0
cdhowie On BEST ANSWER

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());