-
Notifications
You must be signed in to change notification settings - Fork 252
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
Improved Error Handling #625
Conversation
From what crate is Ah, we define it and do have a generic StatusCode: azure-sdk-for-rust/sdk/core/src/errors.rs Lines 119 to 120 in ec80528
Though, shouldn't the subsequent |
This is from the
I'm not sure which /// The kind of error
///
/// The classification of error is intentionally fairly coarse.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ErrorKind<O> {
/// An error specific to a certain operation
Operation(O),
/// An HTTP status code that was not expected
UnexpectedOperation { status: u16 },
/// An error performing IO
Io,
/// An error converting data
DataConversion,
/// An error getting an API credential token
Credential,
/// A catch all for other kinds of errors
Other,
} Every operation is expected to return a #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Expected errors which may be returned by the `CreateDatabase` operation.
pub enum CreateDatabaseError {
/// Returned when the JSON body is invalid. Check for missing curly brackets or quotes.
BadRequest,
/// Returned when the ID provided for the new database has been taken by an existing database.
Conflict,
} If the server returns a statuscode which doesn't match any of the expected error kinds, we then assign it to an error with kind Other error kinds such as Overall it makes sense that this is quite a bit to cover, and I'm hoping we can find some time in the sync meeting later today to walk through how the error handling scheme we propose in this PR works. |
sdk/core/src/error/mod.rs
Outdated
|
||
/// An extention to the `Result` type that easy allows creating `Error` values from exsiting errors | ||
/// | ||
/// This trait should not be implemented on custom types and is meant for usage with `Result` |
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.
Could use the sealed trait pattern to enforce this
This PR is in a state where we could merge it. However, there are additional elements that I would like to follow up on - either in this PR or a subsequent one:
|
Going to merge this. For the above three points, we decided:
|
This is a proposal for a new form of error handling in the SDKs I have been working on with @yoshuawuyts. This is an attempt to reduce the large amount of errors into as compact a form as possible while still offering good ergonomics and quality error messages.
In the grand scheme of things, this proposal is not a radical departure from the current approach taken, but is an attempt to rethink error handling from first principles.
The current approach
The current approach to error handling is to create a large, all-encompassing error type modeled as a Rust enum. This looks like this:
This approach does have some nice properties:
thiserror
crate makes it pretty easy to declare new error variants together with how they are displayed to the user with minimal boiler plate and that conform to best practices for error types including provide an implementation for theError::source
method which shows what other error caused this error.non_exhaustive
.However, this approach does have downsides:
Error
in question is tied directly to the underlying data or error cause. For example, in the code above, anError::Parse
must always have a correspondingParseError
. This encourages an ever expanding tree of errors.ParseError
type that explains this. This adds even more pressure for an ever expanding tree of error types.The above list is not exhaustive but in general should give you the impression that the way we're modelling errors encourages error types with lots of variants and sub errors. This is exactly what we see in practice, where the error types in
azure_core
are bloated and often hard to keep track of.What do we need?
Before getting into solutions, I'd like to briefly discuss the general needs we have from our errors. Errors are generally used for two things:
The more important of these two for our purposes is likely error messages. While no doubt, users of the library will want to write code that inspects some errors and handles them in special ways, the large majority of errors will be propagated up the stack either through use of
?
or by panicking.Therefore, we need errors that can handle control flow but more importantly, encourage good error messages.
The status quo provides ok error messages and excels at handling control flow, the exact opposite of our needs.
A proposed solution
This PR introduces a proposal for a new
Error
type that eventually will completely replace the currentError
type (but that still works with it so that the transition does not need to happen all at once).This new error is based closely on
std::io::Error
. Essentially the new error type wraps other errors and/or an error message along with anErrorKind
that roughly corresponds to the category of error being modeled.First, let's see how this new error handles control flow (arguably the weaker of its capabilities):
Control Flow
As you can see from this example, control flow is still very much possible, but does now rely on runtime downcasting for control flow of errors that we generally expect users to not have to handle. For example, in this case, the user wants to handle
ConnectionAborted
but we expect this to not be a super common use case as the SDK already does automatic retries and a policy will likely be there to handle offline scenarios in the general case.NOTE: the rest of this section is no longer true and has been removed from the design.
The biggest difference with the current design is that the newError
is actually generic over some type that represents operation specific errors. For example, if we're dealing with an error in the Cosmoscreate_database
operation, the error type would beError<CreateDatabaseError>
. TheCreateDatabaseError
would include all the documented cases specific toCreateDatabase
- for example, a bad request or a conflict. These errors are the ones we generally expect users to want to handle, and they are handled in a first class way:Error messages
The new
Error
type comes with an improved ability to provide good error messages. TheError
type conforms to best practices of thestd::error::Error
including providing asource
for each error and displaying errors in such a way that plays nicely with ecosystem crates likeeyre
for displaying good error messages.Included are conveniences like extensions to the
Result
type which allow for providing additional context to the error:Comparison
The new error type improves upon the old error type by not encouraging a growing tree of error types. It provides conveniences for providing good error messages while prioritizing a small error type that plays well with the ecosystem, provides good error messages, and provides control flow handling for cases where that handling is most likely needed.