Skip to content
This repository has been archived by the owner on Sep 12, 2024. It is now read-only.

Error Handling FAQ #50

Open
yaahc opened this issue Apr 6, 2022 · 31 comments
Open

Error Handling FAQ #50

yaahc opened this issue Apr 6, 2022 · 31 comments

Comments

@yaahc
Copy link
Member

yaahc commented Apr 6, 2022

Reducing confusion around error handling in the rust-lang ecosystem is one of the top priorities for the error handling project group this year. As part of that effort we're going to start maintaining a list of Frequently Asked Questions about error handling.

For now this issue will serve as a living document for these FAQs, though eventually I hope to move them to a move permanent / visible location.

Unanswered Questions

If you have any questions that aren't covered by this list, or if you feel any of the answers here fail to completely answer their question for you please let us know so we can improve this list. You can follow up the following ways:

Frequently Asked Questions

When should I use panic! vs Result?

Proposed Questions

How should I structure my error types?

TODO (complex and situational)

My current plan is to do a series of case studies on user codebases in various domains. If you have a codebase that you think would be a good candidate for a case study, either because you feel it has optimal error handling that should be used as an ideal example or because it needs improvement, please reach out so I can add you to the list of potential candidates.

  • Requirements:
    • must be able and willing to test nightly or experimental features when relevant, e.g. generic member access, try traits, error in panic, etc (tho you don't need to be willing to commit changes to master or anything)
    • maintainers must have time to spare to provide feedback at multiple stages of the case study

There are many different error handling libraries in use, such as quick-error, error-chain, failure, snafu, thiserror, anyhow, and eyre. [I'm just listing those in the order in which I think they were released.] How do they differ? What different use-cases do they target?

TODO

  • update error handling library section in awesome-rust, cross link to it, provide a framing around "types of error handling" that libraries provide support for (e.g. error defining vs error reporting) and tag the libraries in awesome-rust with the types of error handling they support.
  • Possibly also link to relevant categories on crates.io and lib.rs for further looking up available error handling libraries.
@8573
Copy link

8573 commented Apr 6, 2022

I'd like to leave one comment and one proposed question.

Comment

I suggest that "index out of bounds" (and "trying to access a location beyond the end of an array" in rust-lang/rust#95660) is an unfortunate example to use for an unrecoverable error that justifies panicking, because many types provide get methods that do handle that case recoverably, allowing combining checking whether an index is valid and indexing by that index into a single method call, supporting the idiomatic pattern-matching style if let Some(x) = v.get(i) { … } over the C-like if i < v.len() { let x = v[i]; … }.

I'm not sure what would be a good example of an unrecoverable error, but maybe apparent memory corruption (making it dangerous to try to recover from within the same process) or reaching "unreachable" code (but then an example of "unreachable" code is needed).

I suppose (without good data) that the most common 'legitimate' use of panicking is for otherwise-recoverable errors in functions of which the return types are constrained by some trait or other API not to be able to return error values.

Proposed question

"There are many different error handling libraries in use, such as quick-error, error-chain, failure, snafu, thiserror, anyhow, and eyre. [I'm just listing those in the order in which I think they were released.] How do they differ? What different use-cases do they target?"

While I think it makes more sense to leave it up to individual library authors to explain their targetted use-cases and maybe how their libraries differ from others', I suggest it could be useful for a central resource

  1. to provide a central list of links to those explanations and
  2. to group these libraries into general categories, like "Minimal" vs "Full-featured", "For programs" vs "For libraries" vs "General-purpose", and maybe "Obsolete" (for error-chain and failure).

@yaahc
Copy link
Member Author

yaahc commented Apr 6, 2022

Comment

<...>

Sounds reasonable! I'd be in favor of further updates to pick a better example. I copied the index out of bounds example straight from the book, so if we're going to update that in the panic! docs we may want to also update it in the book itself.

Proposed question

I'm very hesitant to make concrete suggestions but I agree this is something we should address. I had a plan a while back to improve and reorganize the error handling library section in awesome-rust to basically have a bunch of tags like "error defining" or "error reporting" which we could then apply to each item in the list. I'd be happy to go ahead and make that change now, link it here as part of that question, and generally say "it depends on what you're doing, here is the categorization of needs that we think is important, here's a community maintained list of those categorizations that you can add your libraries too that also contains further resources related to each library" and see if that is helpful enough.

@dtolnay
Copy link
Member

dtolnay commented Apr 6, 2022

Use panic! for unrecoverable errors

use Result for recoverable errors

I have never found this useful as the relevant distinction. In my experience the relevant distinction has been that panicks are for signalling that a bug in the program has been detected, and errors are for anticipated runtime failure modes in a correct program.

Whether any of these is recoverable or unrecoverable is quite a separate thing. For example a bug in the program might be recoverable at a thread boundary or HTTP server request boundary, but not if not called from one of those contexts.

The unrecoverable vs recoverable distinction ends up being circular and unhelpful. Like @8573 mentioned about indexing, imagine someone deciding whether to index using [ or get in a particular spot. If they use get, it's gonna be recoverable because they can match on the result. If they use [, it's gonna panic and they can't recover it, because the program will either abort or unwind and catch_unwind's documentation says it's not supposed to be used for try/catch. So we've basically told them "if you use get then use get; if you use [ then use [".

In contrast it makes sense to look at whether something is a bug or not. For slice indexing, there are places where a specific indexing operation being out of bounds could only occur if the program is buggy, and other places where indexing might be out of bounds because the program is correct but a user-provided config file contained something silly. This is a useful distinction to inform someone on whether they should write [ or get.

@ajguerrer
Copy link

I have a question that falls under the "How should I structure my error types?" category. When designing an Error, should I make one big crate-wide error? should it target a module? or should it be specific to a function, etc?

Looking at #11, I spent some time thinking about std::io::Error. It's a pretty chonky error. I wonder how, for any given function in std that returns an io::Error, you are supposed to reason about which ErrorKind might be returned. I suppose most people let a variant bite them (preferably near a hospital) first or dig into the source code. Maybe for most users, std::io::Errors are "unrecoverable" so they just log and die/fail operation, but the std library is not in a position to assume context, so it favors recoverability.

Doubling back to my original question, if we could turn back time, would it be "better design practice" to split std::io::Error into a hypothetical std::net::Error, std::fs::Error, ect. Should it be even more granular?

@yaahc
Copy link
Member Author

yaahc commented Apr 7, 2022

I have never found this useful as the relevant distinction. In my experience the relevant distinction has been that panicks are for signalling that a bug in the program has been detected, and errors are for anticipated runtime failure modes in a correct program.

To me this feels like a problem with not having shared definitions, but I'm fine with standardizing on "anticipated" vs "unanticipated" instead of "recoverable" vs "non-recoverable". I can see the former being more direct where as the later implies the former in my mind. I don't like focusing on "bug" as much but I don't have a strong justification for this as much as a gut feeling that it's not going to cover all the cases where you'd want to panic, but the only thing that comes to mind is unimplemented!/todo!, which fit very well into the "unanticipated" framing but not as well within a "bug" framing. I agree with your point on the unrecoverable vs recoverable dichotomy not meshing well with the catch_unwind API we provide, and I think this anticipated vs unanticipated dichotomy that you've suggested fits it better.

Overall I guess what I mean to say is 👍 good feedback tyty

Edit: I've gone ahead and updated the FAQ answer in the top level comment. I'll open a PR to update the docs on nightly tomorrow. Not sure when/if I'll get around to updating the book, maybe once I've gotten more positive feedback on this new framing.

@yaahc
Copy link
Member Author

yaahc commented Apr 7, 2022

Doubling back to my original question, if we could turn back time, would it be "better design practice" to split std::io::Error into a hypothetical std::net::Error, std::fs::Error, ect. Should it be even more granular?

So I anticipate this (structuring errors, not the io::Error stuff in particular) being the single biggest question we need to answer and my current plan is to approach this as methodically as possible. I'm planning on conducting a series of case studies to analyze the various approaches people take and get a solid understanding of the pros and cons and when each error design approach (e.g. fine grained vs flat) works best. I already have a few potential candidates for case studies written down but doing a std::io::Error redesign as a crate or fork of rustc and getting some codebases to trial using it sounds like a very interesting case study, but beyond that I don't feel comfortable making assertions at the moment with regards to this specific question.

@dtolnay
Copy link
Member

dtolnay commented Apr 7, 2022

I'm fine with standardizing on "anticipated" vs "unanticipated". […] I don't like focusing on "bug" as much

Use panic! for unanticipated errors which indicate a bug in the program has been detected

use Result for anticipated errors that can reasonably be reacted to

This framing is very different from what I had in mind. Anticipated vs unanticipated isn't a distinction I intended to make in #50 (comment). In reality they are both anticipated. Generally something you didn't anticipate, you didn't write code for, so whether you wrote a panic or an error in the code that you didn't write isn't a question: you wrote neither.

Buggy program vs correct program is the point. When you write an assertion to confirm a precondition, that is an anticipated precondition violation in your buggy caller that you are aiming to catch. All the unintended-to-be-reachable unwraps you find in stuff like BTree internals are about anticipating bugs. (And regarding the unimplemented!/todo! panicks, I would consider incomplete programs to be a subset of buggy programs rather than correct programs, but maybe there is a different word than buggy that captures this better.)

@yaahc
Copy link
Member Author

yaahc commented Apr 7, 2022

This framing is very different from what I had in mind. Anticipated vs unanticipated isn't a distinction I intended to make in #50 (comment). In reality they are both anticipated. Generally something you didn't anticipate, you didn't write code for, so whether you wrote a panic or an error in the code that you didn't write isn't a question: you wrote neither.

In my mind "unanticipated" means "you anticipated that this condition would never occur or should never occur", this ties into why expect is named the way it is, so you can put a description for why you expect it to be okay to panic. I still think I prefer the framing of unanticipated vs anticipated, but I'm open to any framing that further reduces ambiguity and confusion. I think we both agree on when panic is appropriate, it's just the words we are using to describe them that is taking some bikeshedding.

I think what I really want is a word that really mirrors expect, some concise way to say "something we expect to not happen". Bug seems reasonably close approximation of that but I am not thrilled with it.

@Stargateur
Copy link

unexpected ?

@dtolnay
Copy link
Member

dtolnay commented Apr 7, 2022

In my mind "unanticipated" means "you anticipated that this condition would never occur or should never occur"

"would never occur" — I think this inevitably becomes about probability, which I would object to. Anticipating that something would not occur means that you feel it is highly unlikely to occur. Yet, some kinds of buggy programs (panicks) arise more commonly than certain niche runtime failure modes in correct programs. For example at least tens of thousands of people have written Rust code with unintended integer overflows, while 3-4 orders of magnitude fewer people have ever encountered a FilesystemLoop IO error kind. Anticipating that it would never happen is not why we made integer overflows panic. Meanwhile nearly 100% of people would be correct estimating that they'll never see a FilesystemLoop error in their lifetime.

"should never occur" — This is a statement about whether a program is buggy or not. If a program does something that it should not do, i.e. was not supposed to do, we call that a bug. To me it isn't helpful to wrap this behind a word like unanticipated or unexpected, when the bugginess is the essence. Precondition assertions are a good example of why. It's correct to write a panicking assertion even with the full expectation that some fraction of our callers will screw it up and trip the assertion, and will need to fix their code to call us correctly.

@BurntSushi
Copy link
Member

I very strongly agree with @dtolnay here about the recoverable vs unrecoverable thing. I've also never found that distinction to be particularly helpful. The way I've always thought about it is this: if a panic occurs, then there must be a bug. Sometimes it might be appropriate to convert the panic to an error, but it's probably just as likely (if not more so) that the panic indicates some problem with a piece of internal logic somewhere or a case that wasn't handled. Whether you can "recover" from it or not seems like an orthogonal issue.

@yaahc
Copy link
Member Author

yaahc commented Apr 7, 2022

Alright, I updated it to be focused on buggy vs correct programs, let me know what you think of the wording.

@dtolnay
Copy link
Member

dtolnay commented Apr 7, 2022

Use panic! for errors that should never occur and indicate a bug in the program has been detected (e.g. detected memory corruption or reaching an unimplemented! code path)

use Result for errors that represent anticipated runtime failure modes in a correct program (e.g. file not found)

"Detected memory corruption" is not a good example here. I have never seen panicks intentionally used for that. Once you are corrupting memory, that's nearly always UB territory and whether the program panicks or not is no longer something you control. I think either of my examples from my previous comment would be more illustrative: precondition violation or overflowing integer arithmetic.

Separately, the "should never occur" part of this I think is not adding value. I think leaving that out would make a better juxtaposition between detecting buggy program vs failure mode in correct program. We are not making a judgement that Result errors are ones that "should occur" in any sense.

If it sounds like I'll keep having objections until you use exactly the wording from the top of #50 (comment), that's probably correct, since I have been using that framing consistently for at least 2 years (like dtolnay/anyhow#81 (comment)) and it's been the result of a couple years of iteration before that. (Obviously I could still be wrong though, even if my opinion isn't going to change.)

@yaahc
Copy link
Member Author

yaahc commented Apr 7, 2022

There are two reasons I'm having trouble copying your wording verbatim. First, I want to make it clear that panics are a type of error. Second, I want to focus the answer to this question on the interfaces that caused the confusion originally, panic! and Result, not the concepts they represent, aka panicking vs errors.

The first issue is important to me largely because of my error in core work and the unification of &dyn Error and panic! that I talked about in https://blog.rust-lang.org/inside-rust/2021/07/01/What-the-error-handling-project-group-is-working-towards.html. I think of unwrap and expect as constructors for errors that represent bugs where the source error is a different error type (E) that represents an anticipated runtime failure. I want to make it so the panic handler can access the &dyn Error as a source when printing errors from unwraps and expects and utilize the full expressive power of the error trait interface when reporting these errors, rather than lossily passing it through Debug. To me this is a fundamental missing operation in rust today, and is very much related to the comment from anyhow that you linked above. The poison error is a source error for bugs 99% of the time. We should have an interface for smoothly transitioning from possibly an anticipated runtime failure to a definite bug. I'm rewording your statement to make it clear when describing both of these interfaces that they represent errors, and attach meaning to the error (signalling a bug vs an anticipated failure mode).

The second one is important because I want to focus on the specific interfaces that caused the confusion that inspired this specific frequently asked question. I want to describe the specific roles of the panic! and Result interfaces, how they relate, and how they differ. The issue to me isn't the "should not occur part" but the "panicks are for signalling, ... errors are for ..." wording in your answer, which is forcing me to try to reword it to talk about the interfaces themselves and I end up adding other bits to make it flow which you end up objecting to.

I'm struggling a bit with succinctly expressing the differences between these two interfaces. The problem being, in my mind panic! is a constructor for a type of error, where as Result is a wrapper that signals that a contained type is a type of error, but it doesn't feel quite accurate to call Result a constructor for errors in it's own right. These interfaces are not exactly analogous but it's hard to succinctly describe this while also giving people the information they need to properly use these interfaces.

Given that, how do you feel about these attempted rephrasings based on your original framing:

  • Short (for the FAQ): "Use the panic! macro to construct errors which signal that bug has been detected in the program, use Result::Err to wrap error types that represent anticipated runtime failure modes in a correct program".
  • Longer (starting point for updating the panic! docs): "The panic! and Result interfaces are not completely analogous. The panic! macro is used to construct errors that represent a bug that has been detected in your program. With panic! you provide a message that describes the bug and the language then constructs an error with that message, reports it, and propagates it for you. Result on the other hand is used to wrap other types that represent either the successful result of some computation, Ok(T), or error types that represent an anticipated runtime failure mode of that computation, Err(E). Result is used alongside user defined types which represent the various anticipated runtime failure modes that the associated computation could encounter. Result must be propagated manually, often with the the help of the ? operator and Try trait, and they must be reported manually, often with the help of the Error trait.

@ojeda
Copy link

ojeda commented Apr 7, 2022

Two points, if I may:

  • Whether a program is correct or not all depends on the requirements. For instance, a program like fn main() { panic!(); } may be correct if the requirement is that it always panics. For instance, for testing a testing framework.

  • Even if "normal" programs are expected to "not panic", panics can still happen in fully correct programs. For instance, a hardware error that makes an assert fail, even if triggering that assert is impossible given Rust semantics.

In short: if a programmer uses a panic, that just means the program does not have a way to handle the error. Whether that is the right choice, and whether that is an actual bug or not if it happens, and whether abort or unwind should be used, etc., it all depends on the requirements.

@yaahc
Copy link
Member Author

yaahc commented Apr 7, 2022

Two points, if I may: <...>

I think this very succinctly touches on some of my discomfort around framing this entirely in terms of correctness vs bugs. That's why I've historically leaned on arbitrary jargon, but clearly that requires shared understanding of definitions that is impossible to achieve universally.

@8573
Copy link

8573 commented Apr 7, 2022

I'm very hesitant to make concrete suggestions

Ah, yes, I didn't mean for the Project Group to endorse particular libraries over others. Of the categorizations I suggested, I wouldn't see any as judgemental (but maybe others would?) except "Obsolete", and that I intended only for libraries that clearly are considered obsolete by their own maintainers (such as by being archived on GitHub in the cases of error-chain and failure).

@dtolnay
Copy link
Member

dtolnay commented Apr 7, 2022

I think both of #50 (comment) are an extreme stretch beyond what it makes sense for this document to take into account.

a program like fn main() { panic!(); } may be correct if the requirement is that it always panics. For instance, for testing a testing framework.

This is a Russell's paradox / Richard's paradox-level loophole. If someone writes software to detect bugs (such as Clippy), does it become impossible for them to test their software, because "write a program that contains a bug" is a requirement that is definitionally impossible to accomplish?

The fix is the same as for all the math paradoxes. Define a logic in a way that does not make reference to the truth or falsity of statements in that logic; define the requirements of a program in a way that does not make reference to that program's own bugginess.

Testing test frameworks, where your program is only right if it's wrong, is not a useful scenario to teach someone about error handling.

panics can still happen in fully correct programs. For instance, a hardware error that makes an assert fail, even if triggering that assert is impossible given Rust semantics.

This one is even more of a stretch. If we subscribe to this, every standard library function would need a caveat that says "except if a cosmic ray makes it do something different this time".

@dtolnay
Copy link
Member

dtolnay commented Apr 7, 2022

@yaahc, #50 (comment) looks terrific to me.

Dylan-DPC added a commit to Dylan-DPC/rust that referenced this issue Apr 7, 2022
reword panic vs result section to remove recoverable vs unrecoverable framing

Based on feedback from the Error Handling FAQ: rust-lang/project-error-handling#50 (comment)

r? `@dtolnay`
@yaahc
Copy link
Member Author

yaahc commented Apr 8, 2022

Another question that came up today:

What wording should I use in Result::expect/Option::expect?

This one I'm hesitant to be to prescriptive about, because I know people use a lot of different styles for constructing error messages via expect. I am interested in seeing if there is any sort of consensus for how these should be used, or any further additions we could make to the docs that would help guide our users to confidently write expect msgs, even if that means just documenting all of the approaches and their advantages / disadvantages.

I am aware of two common styles for how people utilize word expect messages.

  1. Use it to report an error message
  2. Use it to describe the precondition that should prevent it from panicking

I've mocked up a semi realistic example of both approaches here to compare their output:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6bef1bf9b8498aaccb37a4a8214da032

Running these examples produces the following output

---- expect_as_precondition stdout ----
thread 'expect_as_precondition' panicked at 'user provided input will always be a string containing a valid u32: ParseIntError { kind: InvalidDigit }', src/lib.rs:18:10

---- expect_as_error_message stdout ----
thread 'expect_as_error_message' panicked at 'user provided invalid input: ParseIntError { kind: InvalidDigit }', src/lib.rs:11:10

While I am personally in the "likes to use expect to document preconditions" camp I think it is important to acknowledge that the default output of expect is not really sympathetic to this approach, even if the name of the method itself is. The default output does nothing to acknowledge that the message was an expected precondition, and out of context it makes little sense. I think the trade off here is you end up creating a better experience for developers reviewing the .expect calls in source but you create a worse experience for users viewing your errors.

So I'm curious:

  • Which of these approaches do people prefer?
  • Are there any other approaches I'm not considering here?

@Stargateur
Copy link

Stargateur commented Apr 8, 2022

Personally, I would always use expect for precondition that should be true no matter what user do (minus some good reason).

An the matter of expect subject, I answered a SO question Is there a more concise way to format .expect() message?. The unwrap crates show that very well, I think this is a weakness of .expect() (opposed to panic!() feature) specially if you want to use it as user error message.

Dylan-DPC added a commit to Dylan-DPC/rust that referenced this issue Apr 8, 2022
reword panic vs result section to remove recoverable vs unrecoverable framing

Based on feedback from the Error Handling FAQ: rust-lang/project-error-handling#50 (comment)

r? ``@dtolnay``
@ojeda
Copy link

ojeda commented Apr 8, 2022

I think both of #50 (comment) are an extreme stretch beyond what it makes sense for this document to take into account.

They are not an "extreme stretch", not even for pedagogical matters.

This is a Russell's paradox / Richard's paradox-level loophole. If someone writes software to detect bugs (such as Clippy), does it become impossible for them to test their software, because "write a program that contains a bug" is a requirement that is definitionally impossible to accomplish?

A tool like Clippy may have tests that contain a pattern that is considered undesirable or a bug in normal programs -- that is just fine, no loophole there.

If you do not like that counterexample, there are many others. Some even arise in common situations. For instance, anybody writing "scripts" for themselves may decide to use unwrap() because they do not need any handling for certain cases, thus adding extra paths and complexity would not be optimal. The fact that one can trigger a panic in such a script does not mean the script is buggy. The script is correct as long as that is what was designed to do.

Testing test frameworks, where your program is only right if it's wrong, is not a useful scenario to teach someone about error handling.

To me, it sounds like you are assuming panics are "wrong". That is the root of the problem. Panics are not wrong. In fact, they are the best solution in some cases.

This one is even more of a stretch. If we subscribe to this, every standard library function would need a caveat that says "except if a cosmic ray makes it do something different this time".

No, the standard library is not claiming to guarantee anything outside Rust semantics.

And, to be clear, my points are not about writing those precise examples in the documentation, but to avoid identifying "correctness" with "no panics".

Dylan-DPC added a commit to Dylan-DPC/rust that referenced this issue Apr 9, 2022
reword panic vs result section to remove recoverable vs unrecoverable framing

Based on feedback from the Error Handling FAQ: rust-lang/project-error-handling#50 (comment)

r? ```@dtolnay```
@ajguerrer
Copy link

ajguerrer commented Apr 9, 2022

@ojeda touches on something I was thinking about. If panic! indicates the presence of a bug, then wouldn't developers just fix the bug and choose not get themselves in a situation where they are typing panic!? I think it might be valuable to suggest that panic! is a convenience tool for signaling a bug where it is more desirable - effort wise or behaviorally - to halt the program than fix the bug. One example effort wise is scripts, where development speed is favored over robustness. Another example behaviorally is runtimes/frameworks. I suspect that the tokio runtime entry point, block_on, deliberately chooses to panic! instead of return a Result because users are not expected to be able to handle issues that may arise within tokio's machinery.

@8573
Copy link

8573 commented Apr 9, 2022

If panic! indicates the presence of a bug, then wouldn't developers just fix the bug and choose not get themselves in a situation where they are typing panic!?

I would imagine that the developer typing panic! commonly doesn't know where a bug might be that would violate their assumption and trigger the panic — elsewhere in their crate, in a dependency, in a dependent, or maybe nowhere.

@yaahc
Copy link
Member Author

yaahc commented Apr 9, 2022

@ojeda am I interpreting your concern correctly if make it so we talk about it in terms of "panic should be used for errors that represent bugs, result should be used for errors that represent anticipated runtime failure modes" but leave correctness out of it and hopefully imply that should doesn't mean must?

@nagashi
Copy link
Contributor

nagashi commented Apr 9, 2022 via email

Dylan-DPC added a commit to Dylan-DPC/rust that referenced this issue Apr 9, 2022
reword panic vs result section to remove recoverable vs unrecoverable framing

Based on feedback from the Error Handling FAQ: rust-lang/project-error-handling#50 (comment)

r? ````@dtolnay````
@ojeda
Copy link

ojeda commented Apr 9, 2022

@ojeda am I interpreting your concern correctly if make it so we talk about it in terms of "panic should be used for errors that represent bugs, result should be used for errors that represent anticipated runtime failure modes" but leave correctness out of it and hopefully imply that should doesn't mean must?

Yeah, I think that would be better.

An alternative could be to introduce panics without a mention to bugs right away, but as a way to diverge abnormally. They may be used for different purposes, and reaching them may be a bug in the code itself (e.g. reaching an unexpected logic state, the unanticipated failure of an operation, a precondition violation, etc.) or not (e.g. misuse of a program within a bigger system, a situation that was not required to be handled, testing purposes, documentation examples, soft errors and hardware faults in real life executions, compiler bugs, etc.).

@yaahc
Copy link
Member Author

yaahc commented Apr 14, 2022

I've gone ahead and created an update to Result::expect's documentation based on my comment above.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue May 26, 2022
Add section on common message styles for Result::expect

Based on a question from rust-lang/project-error-handling#50 (comment)

~~One thing I haven't decided on yet, should I duplicate this section on `Option::expect`, link to this section, or move it somewhere else and link to that location from both docs?~~: I ended up moving the section to `std::error` and referencing it from both `Result::expect` and `Option::expect`'s docs.

I think this section, when combined with the similar update I made on [`std::panic!`](https://doc.rust-lang.org/nightly/std/macro.panic.html#when-to-use-panic-vs-result) implies that we should possibly more aggressively encourage and support the "expect as precondition" style described in this section. The consensus among the libs team seems to be that panic should be used for bugs, not expected potential failure modes. The "expect as error message" style seems to align better with the panic for unrecoverable errors style where they're seen as normal errors where the only difference is a desire to kill the current execution unit (aka erlang style error handling). I'm wondering if we should be providing a panic hook similar to `human-panic` or more strongly recommending the "expect as precondition" style of expect message.
jtroo added a commit to jtroo/kanata that referenced this issue Aug 10, 2022
All expect messages are adjusted or replaced with unwrap following the
guidelines here:

rust-lang/project-error-handling#50
rust-lang/rust#96033
jtroo added a commit to jtroo/kanata that referenced this issue Aug 10, 2022
All expect messages are adjusted or replaced with unwrap following the
guidelines here:

rust-lang/project-error-handling#50
rust-lang/rust#96033
jtroo added a commit to jtroo/kanata that referenced this issue Aug 10, 2022
All expect messages are adjusted or replaced with unwrap following the
guidelines here:

rust-lang/project-error-handling#50
rust-lang/rust#96033
workingjubilee pushed a commit to tcdi/postgrestd that referenced this issue Sep 15, 2022
reword panic vs result section to remove recoverable vs unrecoverable framing

Based on feedback from the Error Handling FAQ: rust-lang/project-error-handling#50 (comment)

r? ````@dtolnay````
workingjubilee pushed a commit to tcdi/postgrestd that referenced this issue Sep 15, 2022
Add section on common message styles for Result::expect

Based on a question from rust-lang/project-error-handling#50 (comment)

~~One thing I haven't decided on yet, should I duplicate this section on `Option::expect`, link to this section, or move it somewhere else and link to that location from both docs?~~: I ended up moving the section to `std::error` and referencing it from both `Result::expect` and `Option::expect`'s docs.

I think this section, when combined with the similar update I made on [`std::panic!`](https://doc.rust-lang.org/nightly/std/macro.panic.html#when-to-use-panic-vs-result) implies that we should possibly more aggressively encourage and support the "expect as precondition" style described in this section. The consensus among the libs team seems to be that panic should be used for bugs, not expected potential failure modes. The "expect as error message" style seems to align better with the panic for unrecoverable errors style where they're seen as normal errors where the only difference is a desire to kill the current execution unit (aka erlang style error handling). I'm wondering if we should be providing a panic hook similar to `human-panic` or more strongly recommending the "expect as precondition" style of expect message.
@saona-raimundo
Copy link

I hope this is the right place to ask.
How should one deal with a collection of errors? Say Vec<Error>?

This is a common error in parsers and it does not fit the Error trait well.

@yaahc
Copy link
Member Author

yaahc commented Apr 5, 2023

@saona-raimundo

That usecase is one of the motivations behind the design of the provider API: rust-lang/rust#96024

Here's the companion generic member access RFC that I still need to update and actually get merged1: rust-lang/rfcs#2895, or more specifically the third bullet of this subsection: https://github.com/yaahc/rfcs/blob/master/text/0000-dyn-error-generic-member-access.md#example-use-cases-this-enables

error source trees instead of chains by accessing the source of an error as a slice of errors rather than as a single error, such as a set of errors caused when parsing a file TODO reword

It doesn't really make it easy, nor does it actually enforce any standard way of accessing the chain of errors that exist at the same level, you could in theory have one library that provides their vec of sources as an iterator type and another that provides them as a slice, and you have to know what types to request which causes some potential footguns. However, it does make it possible to pass this information out across a dyn Error trait boundary so long as you know the explicit types (akin to downcasting and Any).

There are some other patterns I can recommend depending on the specific use case, and whether or not you need to extract the vec of errors thru a dyn Error. Happy to info dump more and help you work through possible approaches, but sadly I think the current answer to this question in Rust is that we don't have a good way to deal with that situation other than avoiding the Error trait2.

Footnotes

  1. there's never enough time in the day I swear to god >_<

  2. Actually, I don't think that counts as a good way either.

@saona-raimundo
Copy link

Thanks a lot, I think I understand the RFC better!

My current understanding

(Please let me know if I got it all wrong!)

Provider/Demand RFC rust-lang/rust#96024

The Provider/Demand API is nice by itself, even without talking about errors.
Motivated by handling errors better, there is the introduction of "errors should implement the Provider trait".
In a different chronological order, we may have had something like the following trait bound:
trait std::error::Error: std::any::Provider
But, of course, introducing this trait bound would break a lot of code, so the blanket implementation is proposed.

// core::error
impl<E: Error + ?Sized> Provider for E;

With this, any type implementing the Error trait also implements the Provider API.

The companion RFC rust-lang/rfcs#2895

At first sight, I thought that it is just the "convenience" methods after "errors implement the Provider trait".
But it is not only that.

  1. With a blanket implementation of Provide for errors, one can not overwrite it. On the other hand, a method of the Error trait can be specialized, namely provide_context.
  2. There are inherent methods on dyn Error for easy querying context easily, instead of going through a Demand (which would be the case after only the main RFC(?)).

Back to returning multiple errors

For my case, it seems that the following holds.

  1. While coding a parser that may record multiple errors, it is okay (for now) to return Vec<MyError>, where MyError implements Error and is well documented. This is okay even if Vec<MyError> does not implement the (current) Error trait.
  2. If the RFC gets accepted, Vec<MyError> should be replaced by something that implements the new Error trait and documents that the attached context is Vec<MyError>.

Questions

  1. Is provide_context just an "alias" for the blanket implementation of provide? What would be the difference? In other words, if the companion RFC is accepted, why would one need the main RFC to include impl<E: Error + ?Sized> Provider for E;?
  2. Is the idea that the Error trait still has a source method? If so, should it be implemented (specialized) only when there is a unique source? For example, if there are many possible sources, should source return any (the first one) of the possible sources?
  3. About the experimental std::error::Report, its current design works with a chain of errors. With the new Error trait, this Report may lose its functionality since it may not know what context it should request. A "generic formattable report" that works with any context may not be possible. Will the guideline be that each (binary) crate codes its own Report type, and that std::error::Report is used only if one expects a chain (and not a tree) of errors?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants