7 ms·
Author of `wherr` here. The idea is to minimise the effort needed to debug programs that already make use of the `?` operator, when the underlying error doesn't
by JoelJacobson 3y ago
Author of `wherr` here. The idea is to minimise the effort needed to debug programs that already make use of the `?` operator, when the underlying error doesn't already contain file and line information, where the error is possibly coming from code of of your own control. Sure, you could catch such errors and return a refined error type, but that's more code, which might be what you want in the end, but this is for the code that currently makes use of the simple `?` operator.
Once the file and line where the error is returned has been identified (using `wherr`), it might be a good idea, like you suggest, to introduce a new error type or refine the existing one. But maybe the error is fine as it is, and you just needed to know where it came from, to fix some bug, that when fixed, will cause the error to not happen any longer. Basically, when feeling confident an error cannot happen, some people use `.unwrap()` or `.expect()`, which *do* reveal the file and line, but then the program also panics. This might be what you want, but if you're developing a library, you probably most times want to let the "user" (the developer using the library) decide what course of action to take, based on the error. However, that "user" might not be at all interested in the file and line information of where the error originates from in your library source code, since the user might not be capable of fixing the possible bug anyway. But maybe the user wants to file a bug report, and then it would be helpful to know the file and line information.
> What about all the other places where everything went right? Wanna log those, too? :)
If everything went right, the Ok(val) is simply returned as Ok(val) unchanged, see `wherr/src/lib.rs`:
match result {
Ok(val) => Ok(val),
...
- dannymi 3y agoThis crate is nice to have for debugging. Thank you for writing it. I just had the case already over the past decades where everyone wants to turn every error into Java backtraces (even when not in Java ;) ) and want to emphasize that this is almost never what one should do in regular operation. As you said, unwrap() and expect() and panic!() do that for the "definitely a bug" case already. And, there, it's correct. >probably most times want to let the "user" (the developer using the library) decide what course of action to take, based on the error. I agree. >But maybe the user wants to file a bug report, and then it would be helpful to know the file and line information. In my opinion this belongs in the debug information (i.e. dwarf or similar) then. You can also store dwarf debug info outside of the object file. >> What about all the other places where everything went right? Wanna log those, too? :) >If everything went right, the Ok(val) is simply returned as Ok(val) unchanged, see `wherr/src/lib.rs`: match result { Ok(val) => Ok(val), ... I see. But what I meant is that if the Err case of a regular Result value is enriched like this, why not the Ok case? After all, the programmer (user of your crate) could have erroneously returned Ok where they should have returned Err. Would only be consistent. I know that that isn't easily possible there. That brings me back to "all result::Result construction sites should show up in dwarf debug info". I use debuggers less than I should. But thinking about it that's silly. We should be able to use debuggers to debug problems, including problems like this.
- dannymi 3y agoOpen bug report about inability to set a breakpoint on Err: https://github.com/rust-lang/rust/issues/54144 https://github.com/rust-lang/rust/issues/54144
- simiones 3y agoIt seems that you are trying to take the position that Ok/Err are similar to if/else in that neither is more or less expected than the other. The very names chosen in Rust show that this is not how most think about these two cases. Returning Err is for cases that the developer considered errors, abnormal situations. This attitude that the happy-case and the error-case paths are equivalent and should be treated the same is bizarre and seems to be contradicted by virtually every language choice, even in communities that profess it. Go is probably the most notorious example of professing "errors are just regular values" and refusing to add any error-handling constructs to the language; and yet, even in Go, errors are called, well, `error`s and there are explicit patterns that everyone follows that would never be considered acceptable for regular values (such as naming all error values "err" and reusing the "err" variable to hold any error returned by any function in the same context - try that with a "ret" to hold any non-err return value and see how many people accept your code).
- segfaltnh 3y agoBut errors that are returned by functions are expected behavior. Except for where you assert its bizarre you seem to support this position. The only semantic thing rust offers here is '?', and that's just syntax sugar for errors are values. Truly exceptional situations are those we don't expect, and for that we panic. I expect errors, because it's a computer and that's what they do.
- simiones 3y agoNo, the error path is a distinct concept from the success path, in almost all situations. The success path implements some kind of business logic, and will be highly specific to the task at hand. The error path almost always does nothing more than returning early with an indication that the operation failed (with some context about the failure) to an entity above, ultimately to the user. A minority of cases may involve an automatic retry of the operation. Anything else is vanishingly rare. As such, virtually all languages separate the business logic from the error logic. They offer exceptions, or Rust's ? macro, or enshrine certain error handling patterns (negative return codes in C, if err != nil in Go, use of the Either monad in Haskell).
- pcthrowaway 3y agoExcellent work! Note, this is also possible in tokio-rs/tracing, by using `.with_file()` and `.with_line_number()` to the subscriber creation.. I'm sure you can set up filters/layers for more granular control over this if you don't always want file/line info included, but it's nice to have another option that makes it as easy as using the `#[wherr]` macro, and honestly, tokio-rs/tracing layers and filters are a bit unwieldy. If you don't need that control though and don't have a problem using `tracing` (which is excellent and very powerful btw), is there any reason to reach for wherr instead?
- brunoqc 3y agotokio-rs/tracing is pretty cool with opentelemetry too.
- ryukoposting 3y agoThis crate is an answer to my debugging prayers, thank you for making it. Is there a feature I can toggle to turn off the stack traces? This is a feature that I can see being useful only in development builds for some of my use cases.
- JoelJacobson 3y agoThanks, I'm glad you like it! Not sure what you mean when you say you want to "turn off the stack traces"? There are no implicit stack traces / backtraces. The detailed example in the docs makes use of `.expect()` and `RUST_BACKTRACE=1`, maybe this made you think it always does a backtrace? Here is an example without backtraces, where the debug string is created and printed for the error instead: use wherr::wherr; #[wherr] fn concat_files(path1: &str, path2: &str) -> Result<String, Box<dyn std::error::Error>> { let mut content1 = std::fs::read_to_string(path1)?; let content2 = std::fs::read_to_string(path2)?; content1.push_str(&content2); Ok(content1) } fn main() { let content = concat_files("file1.txt", "file2.txt"); println!("{:?}", content); } % cargo run --example example Compiling wherr v0.1.5 (/Users/joel/src/wherr/wherr) Finished dev [unoptimized + debuginfo] target(s) in 0.15s Running `/Users/joel/src/wherr/target/debug/examples/example` Err(Os { code: 2, kind: NotFound, message: "No such file or directory" } error at wherr/examples/example.rs:5)
- ryukoposting 3y agoAh, I meant "stack traces" in the system sense: Imagine a really deep chain of #[wherr]-marked functions. If I understand the crate's behavior right, when an error occurs in that case, you're effectively constructing a stack trace as that error bubbles up through `?` operators. Now, maybe if the functions return something like Result<_, wherr::Error>, you could collapse those traces, but if you're returning Result<_, Box<dyn Error>>, that isn't possible (barring macro shenanigans).