Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document the need to handle the underlying error source explicitly #59

Closed
edmorley opened this issue Sep 9, 2024 · 5 comments · Fixed by #60
Closed

Document the need to handle the underlying error source explicitly #59

edmorley opened this issue Sep 9, 2024 · 5 comments · Fixed by #60

Comments

@edmorley
Copy link

edmorley commented Sep 9, 2024

Hi! Thank you for this crate.

I recently tried migrating one of my projects from fs to fs-err.

Given the README says:

fs-err is a drop-in replacement for std::fs that provides more helpful messages on errors. Extra information includes which operations was attempted and any involved paths.

...and then gives examples like:

failed to open file `does not exist.txt`
    caused by: The system cannot find the file specified. (os error 2)

...I was expecting the error messages to include additional information out of the box, and for their display representation to look like the above.

However, I found that it actually made our project's user-facing error messages worse in many cases (for an example, see heroku/buildpacks-python#270), since the underlying cause is no longer printed, and now only the operation and filename was. Printing the debug representation showed the underlying error existed, but under source.

Searching for the caused by line shown in the README examples, I found it didn't originate in the display representation in fs-err, but instead is a feature of e.g. anyhow:
https://github.com/dtolnay/anyhow/blob/afe93e7b167d069ac79f2c7f363b919d3793e6ce/src/fmt.rs#L30

This was surprising, since anyhow isn't mentioned in the README.

I was going to file a bug/feature request about including source in the top-level error's display representation, however, found #51 which suggests not including it is by design.

I get the reasoning in that issue (so I'm not necessarily suggesting the fs-err behaviour should be changed), however, this means fs-err isn't quite the drop-in replacement it says it is, and at the very least, I think the README should mention that consumers need to adjust their error logging to print the error source too, if they are not using a library that does it for them (like anyhow).

@schneems
Copy link
Contributor

I was bit by this behavior recently as well. I had it on my backlog to file an issue about it, but I see Ed already has. Thanks Ed. I'll add my experiences:

I've been advocating for migrating more projects over to fs_err and didn't actually realize it was eating the original error message. I.e. this code will panic:

let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("doesnotexist.txt");

let result = std::fs::read(&path);
let error_string = match result {
    Ok(_) => panic!("Expected an error"),
    Err(e) => format!("{e}"),
};

let result = fs_err::read(&path);
let fs_error_string = match result {
    Ok(_) => panic!("Expected an error"),
    Err(e) => format!("{e}"),
};

assert!(
    fs_error_string.contains(&error_string),
    "Expected FS error '{fs_error_string}' to contain '{error_string}' but it did not"
);

with

Expected FS error 'failed to open file `/var/folders/yr/yytf3z3n3q336f1tj2b2j0gw0000gn/T/.tmpkzhKG9/doesnotexist.txt`' to contain 'No such file or directory (os error 2)' but it did not

The main motivation for switching to fs_err (for me) is to reduce time spent debugging so we don't have to hunt for which file failed to do what operation. However without the original error text we now have to do additional debugging to determine if it's because that file doesn't exist or if permissions don't allow it etc.

Moving forwards

Adding docs to spell out the behavior and explicit recommendations could be good. We could also introduce some behavior changes behind feature flags.

If we wanted to preserve the existing default the feature could be include_source and adding that flag would add back in the original error message. If we're open to a change in defaults then I would suggest printing it by default and then letting anyhow users discover there's a hide_source feature they could toggle on to not get duplicate information.

I'm biased in that this is my main use case (not using anyhow), I also think if you consider the two failure modes it's better to have duplicate information that someone can look up docs for how to disable than it is to have insufficient information and not realize they're missing anything. I am therefore advocating for:

  • Changing the default behavior to show the source
  • Adding a hide_source feature flag
  • Document the new flag and behavior
  • Rev a major version (if we want to be really safe, anyhow users would be more likely to look at the changelog and use the new flag if they desire)

What do you think @andrewhickman? Would you be willing to review a PR for something like that?

schneems added a commit to schneems/fs-err that referenced this issue Oct 17, 2024
close andrewhickman#59

Changes the default behavior so that the output in the readme now matches the output a user would see when they use the library. Specifically, it now includes the original `std::io::Error` in the output.

It introduces a feature `anyhow`. By default:

- By default: fs-err will include `std::io::Error` in the Display output, and return `None` from `Error::source()`
- With `anyhow` fs-err will not include std::io::Error in the Display output, and return `Some(std::io::Error)` from `Error::source()`

This is based on the guidance from andrewhickman#51. That discussion links to this 2020 discussion rust-lang/project-error-handling#23 that suggests that you should either print the source and return `None` from `Error::source()` (https://doc.rust-lang.org/std/error/trait.Error.html#method.source) or not print anything and return `Some(E)`.

This allows users of anyhow (or similar) libraries to configure fs-err for the behavior they desire, while not hiding information by default from unsuspecting adopters that are not using those libraries. It optimizes for the case of accidentally showing extra information that a user can then investigate and disable, rather than hiding information that the user might not realize is missing.
@andrewhickman
Copy link
Owner

Sorry for the late reply, I've been struggling to decide what the correct behaviour is here. I think I'm convinced that the flag in #60 is the correct behaviour, I'll probably release it with a major version bump since it is technically a breaking change.

I'm still pondering on the feature flag name - anyhow feels unintuitive since its not adding a dependency on the anyhow crate. Perhaps something that emphasizes that it causes the Error::source method to start returning something (since features are supposed to be additive), like include_error_source?

@schneems
Copy link
Contributor

schneems commented Oct 18, 2024

Short: I agree "anyhow" isn't great. Something implementation hinting like include_error_source only makes sense in half the internals. I like a "behavior" focused feature name. I'm leaning towards custom_caused_by to hint that the thing it enables is giving control of "caused by" output to the caller to customize it however they like.

Long:
While implementing this, one semantic sticking point seems to be the feature's name versus the two different toggles (returning Some from source() versus displaying the error on Display).

What I mean by that is include_error_source could be interpreted as either "include error source in the display" or "include error source in Error::source()`. But because the feature enables one and disables the other, it will always seem "off" when we view one piece of code in isolation, for example:

        #[cfg(not(feature = "include_error_source"))]
        write!(formatter, "    caused by: {}", self.source)?;

It seems slightly odd that we emit the error source only when include_error_source is not enabled.

I reached for "anyhow" as it conveyed less implementation detail, and more user intent, but I also agree that if no one uses "anyhow" in 5 years, it would become an anachronism (or a Skeuomorph like the using a floppy as the save icon).

For naming problems (a historically difficult thing in computer science), I like to come up with as many possible combinations as possible. In brainstorming, quantity has a quality all of its own.

Behavior:

  • caller_source_display
  • wrapper_source_display
  • error_source_control
  • manual_cause_by
  • cause_by_control
  • control_cause_by
  • manage_caused_by
  • bespoke_caused_by
  • custom_caused_by
  • something else

Literal implications:

  • some_source_skip_display
  • include_error_source
  • some_error_source
  • something else

Esoteric:

  • error_sauce
  • anyhow
  • something else

I'm open to other ideas. A thesaurus can help inspire variations as well. I'm still leaning towards the "behavior" category as it gives us a little room to adjust implementation decisions if needed in the future. I like "caused by" as that's how the end user will experience a difference (and pulling from language used in the current readme).

I'm trying to talk through my thought process, ultimately you're the maintainer and I'll go with whatever you decide.

Edit: Had a moment of Semantic satiation and accidentally swapped "caused" with "called" in my original post. I just fixed it.

schneems added a commit to schneems/fs-err that referenced this issue Oct 18, 2024
close andrewhickman#59

Changes the default behavior so that the output in the readme now matches the output a user would see when they use the library. Specifically, it now includes the original `std::io::Error` in the output.

It introduces a feature `anyhow`. By default:

- By default: fs-err will include `std::io::Error` in the Display output, and return `None` from `Error::source()`
- With `anyhow` fs-err will not include std::io::Error in the Display output, and return `Some(std::io::Error)` from `Error::source()`

This is based on the guidance from andrewhickman#51. That discussion links to this 2020 discussion rust-lang/project-error-handling#23 that suggests that you should either print the source and return `None` from `Error::source()` (https://doc.rust-lang.org/std/error/trait.Error.html#method.source) or not print anything and return `Some(E)`.

This allows users of anyhow (or similar) libraries to configure fs-err for the behavior they desire, while not hiding information by default from unsuspecting adopters that are not using those libraries. It optimizes for the case of accidentally showing extra information that a user can then investigate and disable, rather than hiding information that the user might not realize is missing.
schneems added a commit to schneems/fs-err that referenced this issue Oct 18, 2024
I discussed the logic here andrewhickman#59 (comment). 

The prior suggested name "anyhow" was a little too specific/esoteric and in the event that the feature flag outlives the popularity of `anyhow` it wouldn't make sense.

This suggested name tries to focus on the behavior outcome of the feature. It empowers callers to format the "called by" information however they want by: 

- Providing a source of that data (Error::source)
- Not formatting emitting that data in `Display`
schneems added a commit to schneems/fs-err that referenced this issue Oct 18, 2024
close andrewhickman#59

Changes the default behavior so that the output in the readme now matches the output a user would see when they use the library. Specifically, it now includes the original `std::io::Error` in the output.

It introduces a feature `anyhow`. By default:

- By default: fs-err will include `std::io::Error` in the Display output, and return `None` from `Error::source()`
- With `anyhow` fs-err will not include std::io::Error in the Display output, and return `Some(std::io::Error)` from `Error::source()`

This is based on the guidance from andrewhickman#51. That discussion links to this 2020 discussion rust-lang/project-error-handling#23 that suggests that you should either print the source and return `None` from `Error::source()` (https://doc.rust-lang.org/std/error/trait.Error.html#method.source) or not print anything and return `Some(E)`.

This allows users of anyhow (or similar) libraries to configure fs-err for the behavior they desire, while not hiding information by default from unsuspecting adopters that are not using those libraries. It optimizes for the case of accidentally showing extra information that a user can then investigate and disable, rather than hiding information that the user might not realize is missing.
schneems added a commit to schneems/fs-err that referenced this issue Oct 18, 2024
I discussed the logic here andrewhickman#59 (comment).

The prior suggested name "anyhow" was a little too specific/esoteric and in the event that the feature flag outlives the popularity of `anyhow` it wouldn't make sense.

This suggested name tries to focus on the behavior outcome of the feature. It empowers callers to format the "called by" information however they want by:

- Providing a source of that data (Error::source)
- Not formatting emitting that data in `Display`
@schneems
Copy link
Contributor

I marked #60 as ready for review

andrewhickman pushed a commit that referenced this issue Oct 23, 2024
* Show source by default, add feature to hide it

close #59

Changes the default behavior so that the output in the readme now matches the output a user would see when they use the library. Specifically, it now includes the original `std::io::Error` in the output.

It introduces a feature `anyhow`. By default:

- By default: fs-err will include `std::io::Error` in the Display output, and return `None` from `Error::source()`
- With `anyhow` fs-err will not include std::io::Error in the Display output, and return `Some(std::io::Error)` from `Error::source()`

This is based on the guidance from #51. That discussion links to this 2020 discussion rust-lang/project-error-handling#23 that suggests that you should either print the source and return `None` from `Error::source()` (https://doc.rust-lang.org/std/error/trait.Error.html#method.source) or not print anything and return `Some(E)`.

This allows users of anyhow (or similar) libraries to configure fs-err for the behavior they desire, while not hiding information by default from unsuspecting adopters that are not using those libraries. It optimizes for the case of accidentally showing extra information that a user can then investigate and disable, rather than hiding information that the user might not realize is missing.

* Update feature name

I discussed the logic here #59 (comment).

The prior suggested name "anyhow" was a little too specific/esoteric and in the event that the feature flag outlives the popularity of `anyhow` it wouldn't make sense.

This suggested name tries to focus on the behavior outcome of the feature. It empowers callers to format the "called by" information however they want by:

- Providing a source of that data (Error::source)
- Not formatting emitting that data in `Display`

* Update feature name to `expose_original_error`

* Document why that feature disables displaying error

At a glance it's not clear why that logic exists based solely on the feature name. Adding a doc here clarifies the intent.
@andrewhickman
Copy link
Owner

#60 is released in version 3.0.0 🥳

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

Successfully merging a pull request may close this issue.

3 participants