-
Notifications
You must be signed in to change notification settings - Fork 154
Move from error-chain
to thiserror
#394
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
Move from error-chain
to thiserror
#394
Conversation
error-chain
to thiserror
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR!
I've left some comments, but will take a closer look later this week.
src/errors.rs
Outdated
#[error("{0}")] | ||
Custom(String), | ||
|
||
/// Wraps another error in order to provide more context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, is it a common way of doing this thing?
I see that https://github.com/dtolnay/anyhow for example has a context
function for this. Maybe thiserror
has something similar?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm missing something, thiserror
allows you to add source
for an error variant by one of three ways:
- prepending the field with
#[from]
, - prepending the field with
#[source]
, or - naming the field
source
In all cases, the source of the error variant is kept in the variant itself, and I can't find any way to add this source information alongside the Error
enum in the same way that error-chain
did with the Error
/ErrorKind
setup. I could rename the enum to be ErrorKind
and have an Error
struct that stores the ErrorKind
and a source, but then we are basically reimplementing error-chain
by hand and we lose the benefits of using thiserror
since we won't be able to take advantage of the code generation that it does to allow us to convert those source errors to our Error
enum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It just feels like a something is not right here, either:
thiserror
is not the right tool for the job, or- we should redesign the error types exposed in the library
and I'll need more time to think about this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've got an idea about this that I'll try out today
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking a little more about this, I think using a combination of thiserror
and anyhow
might be the right way to go. thiserror
is good for defining the error types for the library and deriving the Error
trait on them, and anyhow
seems perfect for working with them within the application itself. I'll give that a try and see how it turns out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any updates on this? (also needs merging with master)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we need to add some context to each error, maybe the error type should be
struct Error {
pub error: ErrorKind,
context: &'static str,
}
or something along the lines, or we could add more variants of errors to the lib.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea that's what I'm in the process of moving to. sorry I haven't been more responsive, I'm still working on this and will hopefully have an update for you tomorrow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no worries :) take your time and thank you for the PR!
@ordian ok, I think this might be the way to go. I've kept the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So here we're keeping the lib Error
type, but all functions return anyhow::Result
. Is a user expected to downcast the error to the lib Error
? What are other possible values of the error type besides &'static str
?
TBH I'm not convinced this is an improvement. I would reassess what kind of static string errors we return and maybe create enum variants for them. Also not sure how with_context
messages are helpful in the lib.
Curious what @killercup thinks about this.
if let Some(backtrace) = err.backtrace() { | ||
eprintln!("Backtrace: {:?}", backtrace); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it not possible to keep backtraces with anyhow? :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anyhow can display backtraces as long as it was complied by nightly, since the stdlib backtrace
feature is still unstable. I've tried to convince failure::Fail::backtrace
to accept an anyhow::Error
and try to get a backtrace out of it, but haven't had any luck. anyhow::Error
will deref to a dyn Error + Send + Sync + 'static
, but Fail::backtrace
complains that it is unsized.
I've added a feature that will allow a user to compile with backtrace support as long as they are on nightly, but if there is a better way I'd be more than happy to use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@killercup are backtraces used for identifying bugs in cargo-edit if it fails to run on some input? maybe it's not that helpful for the users after all
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could have sworn that anyhow brought in a dependency on backtrace-rs
on stable but apparently not. I don't mean to shamelessly plug my own stuff but if you want stable backtraces you can swap from anyhow
to eyre
which is a fork that adds support for custom context types, which you could define to include a backtrace::Backtrace
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Losing backtraces is kind of a shame from a technical standpoint, but I don't think they are being relied on by anyone using cargo-edit, because we have such amazing error descriptions! 😉
Kidding aside, I think this is fine. When we finally end up packaging binaries we can compile them with nightly and get some backtraces for free.
@yaahc, I just got what the name is about. Amazing. I'm also totally fine with using your fork. Or switching to it in a later PR.
This is quite the diff, I'll try to read it sometime today. Also cc @yaahc for errors in a small-but-in-production code base :) |
I'm not really clear on which part is the library and which part isn't, assuming everything in
If you're going to immediately erase the error type then there is very little value to using |
thanks @ordian @killercup @yaahc , I think I know where to go with it. |
As @yaahc pointed out, if we immediately erase the error type by converting it to `anyhow::Error` then there's not much reason to have a library error type. This commit puts _all_ errors generated in the library into the Error enum and removes use of `anyhow` from the library completely. Note that `anyhow` is still used in the 3 binaries.
src/errors.rs
Outdated
|
||
/// Failed to parse a version for a dependency | ||
#[error("The version `{0}` for the dependency `{1}` couldn't be parsed")] | ||
ParseReqVersion(String, String, #[source] semver::ReqParseError), | ||
|
||
/// Failed to parse a version for a dependency | ||
#[error("The version `{0}` for the dependency `{1}` couldn't be parsed")] | ||
ParseVersion(String, String, #[source] semver::SemVerError), | ||
|
||
/// An invalid crate version requirement was encountered | ||
#[error("Invalid crate version requirement")] | ||
InvalidCrateVersionReq(#[source] semver::ReqParseError), | ||
|
||
/// Unable to get crate name from a URI | ||
#[error("Unable to obtain crate informations from `{0}`.\n")] | ||
ParseCrateNameFromUri(String), | ||
|
||
/// Unable to get a crate from a git repository | ||
#[error("Failed to fetch crate from git")] | ||
FetchCrateFromGit(#[source] reqwest::Error), | ||
|
||
/// Received an invalid response from a git repository | ||
#[error("Git response not a valid `String`")] | ||
InvalidGitResponse(#[source] std::io::Error), | ||
|
||
/// Could not read manifest contents | ||
#[error("Failed to read manifest contents")] | ||
ManifestReadError(#[source] std::io::Error), | ||
|
||
/// Could not parse Cargo.toml | ||
#[error("Unable to parse Cargo.toml")] | ||
ManifestParseError(#[source] Box<Error>), | ||
|
||
/// Cargo.toml contained invalid TOML | ||
#[error("Manifest not valid TOML")] | ||
ManifestInvalidToml(#[source] toml_edit::TomlError), | ||
|
||
/// Could not found Cargo.toml | ||
#[error("Failed to find Cargo.toml")] | ||
ManifestNotLocated(#[source] std::io::Error), | ||
|
||
/// Could not get cargo metadata | ||
#[error("Failed to get cargo file metadata")] | ||
GetCargoMetadata(#[source] std::io::Error), | ||
|
||
/// Could not get current directory | ||
#[error("Failed to get current directory")] | ||
GetCwd(#[source] std::io::Error), | ||
|
||
/// Could not set output colour | ||
#[error("Failed to set output colour")] | ||
SetOutputColour(#[source] std::io::Error), | ||
|
||
/// Could not write upgrade message | ||
#[error("Failed to write upgrade message")] | ||
WriteUpgradeMessage(#[source] std::io::Error), | ||
|
||
/// Could not clear output colour | ||
#[error("Failed to clear output colour")] | ||
ClearOutputColour(#[source] std::io::Error), | ||
|
||
/// Could not write upgraded versions | ||
#[error("Failed to write upgrade versions")] | ||
WriteUpgradeVersions(#[source] std::io::Error), | ||
|
||
/// Could not print upgrade message | ||
#[error("Failed to print upgrade message")] | ||
PrintUpgradeMessage(#[source] std::io::Error), | ||
|
||
/// Could not truncate Cargo.toml | ||
#[error("Failed to truncate Cargo.toml")] | ||
TruncateCargoToml(#[source] std::io::Error), | ||
|
||
/// Could not write updated Cargo.toml | ||
#[error("Failed to write updated Cargo.toml")] | ||
WriteUpdatedCargoToml(#[source] std::io::Error), | ||
|
||
/// Missing Version Field | ||
#[error("Missing version field")] | ||
MissingVersionField, | ||
|
||
/// Could not write new manifest contents | ||
#[error("Failed to write new manifest contents")] | ||
WriteNewManifestContents(#[source] Box<Error>), | ||
|
||
/// Could not open Cargo.toml | ||
#[error("Unable to open local Cargo.toml")] | ||
OpenLocalManifest(#[source] Box<Error>), | ||
|
||
/// Git repo URL seems incomplete | ||
#[error("Git repo url seems incomplete")] | ||
IncompleteGitUrl, | ||
|
||
/// Could not parse git repo URL | ||
#[error("Unable to parse git repo URL")] | ||
ParseGitUrl, | ||
|
||
/// Found a virtual manifest instead of an actual manifest | ||
#[error("Found virtual manifest, but this command requires running against an actual package in this workspace. Try adding `--workspace`.")] | ||
VirtualManifest, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh boy, I didn't expect that many variants of errors 😅
I understand that on one hand we want to provide as much details about the error as possible, but on the other hand, users of this library won't need to differentiate between SetOutputColour
and WriteUpgradeMessage
. IMHO it's fine to shrink all io errors into one, all toml parse errors into one, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤣 yea, agreed that it's a lot. I've cut it way down in my latest commit, let me know what you think
/// A ReqParseError from the semver crate | ||
#[error(transparent)] | ||
SemVerParse(#[from] semver::ReqParseError), | ||
/// A SemVerError from the semver crate | ||
#[error(transparent)] | ||
SemVer(#[from] semver::SemVerError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we merge these into one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately the source errors are completely separate types so we'd either lose those or have to cast them to trait objects or something
/// Unable to get a crate from a git repository | ||
#[error("Failed to fetch crate from git")] | ||
FetchCrateFromGit(#[source] reqwest::Error), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and this into Reqwest
error
/// Unable to find the specified resource | ||
#[error("The resource `{0}` could not be found.")] | ||
NotFound(String), | ||
|
||
/// Unable to find the specified resource | ||
#[error("The resource `{0}` could not be found in `{1}`.")] | ||
NotFoundIn(String, String), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these to also look similar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I split them up because the second error message is helpful in giving a location for the error, i.e. it ends up looking like "the resource serde
could not be found in dev-dependencies
", and I liked having that location information.
/// Failed to parse a version for a dependency | ||
#[error("The version `{0}` for the dependency `{1}` couldn't be parsed")] | ||
ParseReqVersion(String, String, #[source] semver::ReqParseError), | ||
|
||
/// Failed to parse a version for a dependency | ||
#[error("The version `{0}` for the dependency `{1}` couldn't be parsed")] | ||
ParseVersion(String, String, #[source] semver::SemVerError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and these
/// A value was expected but we got `None` instead | ||
#[error("Expected a value")] | ||
ExpectedValue, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous name ReadHomeDirFailure
was more descriptive, maybe we should remove it and map the err to an io
error?
if let Some(backtrace) = err.backtrace() { | ||
eprintln!("Backtrace: {:?}", backtrace); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@killercup are backtraces used for identifying bugs in cargo-edit if it fails to run on some input? maybe it's not that helpful for the users after all
Error handling is a hot topic in Rust right now -- like very spring, it seems -- and I love the discussion here, this is good stuff. I have two more points. Don't read them as must-haves but just as random suggestions.
|
sorry for the radio silence, I'm still working on this and will try to update here as soon as I can |
I think I'm going to close this...I've tried a couple more things since the last time I updated this PR and haven't been happy with any of them and I don't think I'm going to come back to it. Sorry! |
This is a first attempt at removing
error-chain
and usingthiserror
instead.A couple notes:
Error
enum. Some of the API choices I made in this PR are a result of this difference, I tried to keep the call sites similar as much as possible.Cargo.lock
had quite a few conflicts. Instead of risking trying to resolve them manually, I deleted the lock file and re-created it. Apologies, that seems to have caused quite a large diff for that file.Closes #344