-
Notifications
You must be signed in to change notification settings - Fork 201
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
[RFC] Revamp Error
types
#229
Comments
Yeah #181 is an annoying case, and it would definitely be nice to improve the error handling situation. From quick read / chat I have a few of thoughts:
I might not be totally up to date on this but as far as I am aware There are a couple of possible alternatives that would solve the i2c problem (though could be used in other similar cases) without erasing types / require global allocation: A generic error type with NACK#[non_exhaustive]
pub enum Error<E> {
/// An unspecific bus error occured
BusError<E>,
/// The arbitration was lost, e.g. electrical problems with the clock signal
ArbitrationLoss,
/// A bus operation received a NACK, e.g. due to the addressed device not being online
NoAcknowledge,
...
}
pub trait I2cRead {
/// Error type
type Error;
...
fn try_read(&mut self, address: u8, buffer: &mut [u8]) -> Result<(), Error<Self::Error>>;
} This would work now, allows drivers to handle the i2c appropriate errors, and is consistent with An
|
A real error type for I2C sounds like a good improvement. FWIW, the only error that I have actually needed is Regarding the suitability of a similar My biggest concern with the OP proposal is the point about transportation of underlying errors as @ryankurte already mentioned. I think the #[non_exhaustive]
pub enum Error<E> {
/// An unspecific bus error occurred
BusError,
// ...
/// Something else happened
Other<E>,
} This would distinguish itself from something which is wrong with the bus. However, I think this is not a great solution:
That is why I think that Of course, (at least at first sight) I like the GAT version from @ryankurte the most due to the transparent extensibility but we have been waiting for GAT for a long time and I think I would rather not do that. I am also a bit concerned about the MSRV bump but I personally have no need for an older compiler. I would say |
Thanks for your responses. I don't have time to read them all in detail right now but here's one paragraph which stuck out in the notifcation going by:
I can see other applications as well. For instance if you have an application running e.g. over a UART it would be very helpful being able to see buffer overruns so a driver could start over. For GPIO (where we're discussing things like tri-state GPIOs and changing directions) it might be useful to know that a pin is in a wrong mode for some operation. But even other than in drivers, whenever you're doing real error handling in an application (other than the unfortunately common plain unwrapping) this is currently not at all portable so whenever you want to run an application on a different MCU and properly handle different error situations you'd have to write MCU specific error handling -- which is not great. |
I only found this RFC now, but independently suggested the same solution as @ryankurte / @eldruin: I think it's important that the implementation can provide custom errors if it wants to, but that we can have implementation-independent error variants for things like Nack / ArbitrationLoss / etc so that I²C device drivers can use polling instead of fixed sleep times (when waiting for something to be processed). This should probably be decided before the 1.0.0 release, right? |
Do you have an example for a custom error which would be worth the additional complexity of generic parameter? If we say we would like to entertain the idea of custom errors I'd rather see something like a Custom enum type with a static error string but TBH if we have non-exhaustive errors we can extend them any time if the need arises and everyone gets to profit. Otherwise you'd still have the same handling mess as now and people could decide to rather keep their own proprietary error types instead of shared error types.
Yes. |
From looking through the different HALs, error types that exist right now:
I don't know enough about the different implementations (and SMBus, which seems to add some other error types) to know what all those variants mean, but it feels a bit wrong to throw away error information for error types that we as embedded-hal developers did not think of, or where platform-specific error handling could be added. Maybe some people with more experience can judge that. |
I'm not proposing to throw away error types at all, my proposal is to add all error types (which make sense in some form) into the shared error type. In fact most of the HAL impls only have enum kinds for the errors they're detecting right now, they could provide more of them... |
I also should add that unlike for |
hmm, some other things I didn't really consider in the last reply:
I totally agree that we are missing some API detail, but am still not swayed by the exhaustive (yet non-exhaustive) enum approach. Aside from the other complaints it seems like introducing a bunch of application detail into an otherwise abstract crate and I don't know how we would choose to accept or not-accept enum variants. |
Oh wait, the trait bound is only blocked on GATs where lifetimes are involved (ie. with futures and buffers), so you can pretty trivially implement the second of the options in #229 (comment) on stable. Demonstrated in embedded-hal and stm32f4-hal, should probably try with a consumer / example but, it's time for bed here. |
As far as I am concerned it's up to the application to decide what is recoverable and what is not.
Both. Originally it was supposed to signal that a device does not exist on the bus but nowadays is sometimes is used to signal that the device is busy.
You do have a strange interpretation of "clearer"... 😅
I think both. E.g. if you have a driver talking over some interface and it is waiting for a certain stream of data to arrive and the HAL signals an overrun or a jammed line, the driver might be able to retry the last communication in order to fix the issue. If you don't have a driver but your application is handling the data directly you similarly might want to be able to handle such situations (which is currently not possible in a generic way).
I absolutely disagree. Having a common set of error types for general use is one the achievements of the latest iterations of e.g. libstd. I think it is fundamental for portable and composable software to have a common set of error kinds to rely on and that's also the reason why custom approaches like
Easy: if it makes sense, is not covered by an existing error kind we'll accept it. There should be a very low bar to including new error kinds and since additions are non-breaking I would not worry about that at all. |
I don't think that solves any of the issues and adds more convolution to the API. |
@ryankurte @eldruin So I was looking into your proposal to have a generic custom error kind. I do see a few problems with that approach:
I think with the new fallible-first approach it is very important to deliver a decent set of standard error kinds, too, and not leave it to impls to come up with a wild variety of custom errors (or none at all). |
@therealprof i agree with most your points except for the error nesting bit, ime the flat approach only works in
I think it's also important to remember that this is not only used in
This solves the, there is a specific error that may occur during normal operation of the interface that a driver may wish to handle, problem. Which may be specific to I2C, and in a way that doesn't restrict error types or delete error information, which afaik would resolve #181, and is generalisable to other cases similar to this.
The other question about this is how you ensure people use the correct error types, and how you interpret and use those both at the driver and application level? Say we end up with
A single flat non-generic set would simplify things like this which is nice, but at the same time, seems to make it functionally impossible to report meaningful errors (device disconnect, permissions, pin not mounted, etc.) at the application level, which does not seem to me to be tenable. It would be great to improve the situation, but, I would posit that any improvement should not be worse / less-expressive than the current approach, even if only for a subset of uses :-/ |
By adding another layer (the
To be totally honest: I fully expect some people continuing to blindly I do not expect that applications/drivers will handle each error individually but go "hey, I have a nice way of dealing with this of error (like NACK)" and just use some generic "let's start over" for anything else.
Yes, we should not allow have duplicate types and the description should be very clear so there's no confusion what's what. However any problem around the error kinds in HAL impls, drivers and applications will be trivial to fix in a non-breaking way and can easily done by community contributions. Currently in most impls any change around errors is a breaking change.
I am not proposing a single flat of set of errors, I'm proposing a set of errors per trait. I do not quite see how that makes reporting of any of the above impossible, certainly not any less possible than it is right now. |
Let's take a look at a real public example I know of: I do not know if it would be fine to just start over, but I think if this comes up frequently, it would be useful to the application to be able to know that the MCU pins are acting up, and not the accelerometer, for example, and that is the reason for some failures. This would immediately simplify diagnostics. |
The
Absolutely. |
After reading the comments in addition to the trait specific error types which should cover the peripheral specific interaction with a peripheral it might also make sense to have a general error type for peripheral initialisation to handle the specific errors which usually occur during initialisation only as mentioned by @ryankurte. |
Maybe there is a misunderstanding here. The The point is that it would be interesting for the application to be able to get the the original |
We could support this with this proposal. I do not think this is very practical though to allow passthrough errors and also encourage implementations to make use of them because you have an explosion of possible error cases and the whole point of the proposal is to unify them. What would happen to your application if we did that? Well, if your application uses I2C with a |
Hmm, there is a mismatch in the types. I am not sure if it is intended. If somebody is not interested in causes for the use embedded_hal::i2c;
match i2c.try_read() {
Err(i2c::Error::BusError(_)) => // retry or so, no dependencies on whatever is in there
// ...
} If somebody is interested in some underlying causes, then of course they have a dependency to whatever is inside, but this dependency is already there. e.g. they already know they are using bitbang-hal and that the error might be from the pins the application itself put into it. use embedded_hal::i2c;
match i2c.try_read() {
Err(i2c::Error::BusError(digital::Error(pin))) => // log diagnostics or so
Err(i2c::Error::BusError(e)) => // something else happened
// ...
} So the point is that code that does not care about underlying errors can just ignore them and have no dependencies and code that cares about them can use them. |
I think there are two cases that we need to consider: A generic driver (which will not be able to deal with any platform-specific or project-specific error) and a hardware-specific application. A generic driver will be very happy about unified errors because it allows it to handle certain cases like NACKs. I think nobody disagrees with the idea that there should be a list of predefined error variants. However, an application like the vacuum robot may want to handle different kinds of bus errors differently. If the e-h trait does not allow nesting errors (and thus the error information is not available by using the e-h API), then the driver will need to be modified with a custom non-e-h-compatible interface that returns the needed error information. For example: // Pseudocode
// This is the hardware-specific API with more error information
impl Driver {
fn write_with_error_details(&mut self, bytes: &[u8]) -> Result<(), DriverError> {
...
}
}
// This is the generic driver API
impl Write for Driver {
fn write(&mut self, bytes: &[u8]) -> Result<(), I2cError> {
self.write_with_error_details(bytes)
.map_err(|e| match e {
DriverError::Gpio(_) => I2cError::BusError,
DriverError::I2c(_) => I2cError::BusError,
})
}
} The problem is that now the driver contains "prorietary" APIs that need to be called by the application. The developer of the application would need to fork or copy-paste the driver code in order to modify it. If instead the e-h error enum allows wrapping a generic error, then the driver crate could simply pass through the underlying error, and the application could handle it if desired. Of course this does not mean that we should not try to cover all possible error cases, but it still offers an escape hatch for developers that don't want to go through the "propose-change-to-e-h > get-change-accepted > wait-for-release > update-driver > wait-for-release > update-application" cycle. |
Sorry, just me being sloppy.
That will not work. You either need have a dedicated type for the |
I still don't see it. If you have a universal application then you have no idea (and shouldn't!) what to do about a GPIO error that causes a bit-banging implementation to fail, if the bit-banging driver fails to work around the problem (e.g. by retrying the transaction) then it's game over anyway and you can only tell the application: "Sorry, something's wrong with the I2C bus." If you want to have a proprietary application which cares about those implementation specific details then you can still offer an API in your |
No problem.
Yes, we would need a generic error variant like already proposed: #[non_exhaustive]
pub enum Error<E> {
/// An unspecific bus error occured
BusError(E),
// ...
} This is nothing really new, all drivers already define a generic pub enum Error<E> {
I2C(E),
InvalidInputData,
} Would now become: pub enum Error<E> {
I2C(i2c::Error<E>),
InvalidInputData,
} We now have an unified error layer in between but otherwise the driver code would see very few changes. |
Yes, that's precisely the problem. A user would need to fork all driver crates and modify them with a custom API in order to provide detailed error information. Especially if we want to promote embedded-hal in a commercial environment, it seems a bit weird to first advertise a "production-ready ecosystem of mature generic and compatible drivers" but then say "if you want to do error handling for your hardware-specific project, you will need to fork all generic drivers and adjust them to your needs".
No, embedded-hal can definitely provide a solution for this if we want (the solution with a generic parameter that @eldruin and others proposed). The only question is whether we want to sacrifice this use case in order for the syntax to look slightly more clean (without the type parameter). |
Well, at the moment it has to be generic because the error is in associated type within the HAL impl. I would like to get rid of that, that's part of my proposal. The problem with the generic being all the way generic is that now the application needs to know implementation details which makes the approach less universal. I planned to get rid of the associated error type in the impls, your suggestion would turn that into an inner error type which now needs to be carried around anywhere and quite frankly makes things a lot worse. |
That is not correct. We already have proprietary methods on HAL impls, e.g. to construct them or initialise or make them change something internally (like switching modes on GPIO pins). You will always have implementation details which cannot be governed by
I strongly disagree. We're not sacrificing any usecase here, it's merely a matter of the amount of precision we want to provide out of the box at the cost of complexity. I am of the opinion that simplicity should be the goal. The vast majority of usecases will benefit from the simple approach and only very few specific usecases might actually benefit from the ability to nest errors; do we really want to complicate the implementation and add boilerplate for every user because of some obscure usecases which also could be easily implemented in a different way? |
I already highlighted here what I think the problem with
It seems like it is either overwhelming and thus difficult to use or a small extension of the available variants and thus imprecise. Since generic drivers have no use for I would simply leave |
I do not agree. Have you looked at the available kinds? Any kinds you would add from the top of your head?
Actually they do. Examples include things like handling (or not being able to handle) NACKs, signalling Timeouts, disconnected devices, discovery problems... Those are even more relevant when stacking implementations, e.g. the driver for a GPIO expander connected via SPI would like to signal that a connectivity issue is the problem for your pin toggling problems.
That is an declared anti-goal of this approach, pretty much the only thing I am not okay with. If some system wants to define their own proprietary errors to allow more fine grained handling or better diagnostics that is okay but it should be exception and not the norm. We want to give HAL/driver implementers a solid and consistens toolbox to communicate errors and drivers as well as applications universal and consistent information to allow them to handle any situation in an appropriate way (if they so desire). |
I am sure people will come up with more.
Things like NACKs are already part of the A driver getting an While Since it does not provide a complete solution and is also not useful for generic drivers, I would avoid it and just let I2C methods return I2C-proper errors, or
MCU HALs can reuse an unified error definition like However, I would not make |
Which is encouraged. But if you can't come up with additions in a hurry I have my doubts that we're going to be swamped with an unwieldy number of requests. 😏
Maybe not
It doesn't need to tell exactly what went wrong but if it can point into the right direction that is a huge help. Without it all a non-device specific application can only do a best-effort problem resolution (or just crash), with a useful |
My ability to come up with examples right on the spot is beside the point. A high amount of requests within a short period of time (leading to "swamping") is also beside the point.
Again,
If it can only point into the right direction, you will still need to process the implementation-specific error if you want to fix it or know what actually happened. Why force an additional set of somewhat unrelated errors into every error definition if we already have a mechanism to get the underlying information? |
They are not unrelated, they are general system errors which apply to any peripheral which is exactly why they are in every error definition.
That mechanism is useless in the vast majority of cases because you're back to having device specific error handling which is exactly what we need/want to get away of. I have honestly no idea why some people are insisting on having it but for me this is a compromise I'm willing to make as long as we don't loose the ability to do universal error handling. |
To define the set of enum, what about checking a few existing errors in hal, and see how we would transfer it to the corresponding ErrorKind. For example, the error used in https://github.com/stm32-rs/stm32f4xx-hal/pull/218/files look quite protocol related and should fit in the ErrorKind type. OTOH, the DMABufferNotInDataMemory error in nrf-hal doesn't seem appropriate for a dedicated entry in kind, and, I think, would enter in the linux embedded hal is not really helpful as it seems to directly use
So, maybe we can begin with a small set of variant in the embedded-error kind types, try to use it in a few hal, and see if it need additions. It would be semver compatible. The only thing to really choose are:
|
@TeXitoi I don't think what HAL impls are doing right now is indicative for what they will look like after we make the change. Since all the moment all error types are bespoke there's no right or wrong choice.
That's exactly what I did: Considered a few usecases and defined the variants for them.
The thought behind the name I'd be okay adding an |
But it gives a good idea of the practical errors. I agree that it doesn't show anything in error analysis.
As @eldruin thinks there is too much kinds, I propose to begin with a really minimal set. For example, I didn't find a concrete example of GpioError::WrongMode and ImplError::OutOfMemory for example. We can always add them later, that's not a breaking change.
impl has a meaning in rust, thus I didn't understand the name. As it is added to every error kinds, I thought it represents some kind of error that are applicable everywhere. |
My understanding of the concern is that in the future there will be too many kinds, which I totally disagree with -- there cannot be too many error kinds: when an error can occur and is not represented by a similar kind yet then it should be added and there should be a very low barrier to getting it in. Indeed there's no concrete examples of those kinds yet, mostly because there're only very few implementations at the moment. It makes little sense to me to cut down on error kinds which have been already identified only to add them again later when someone complains about them missing. I do already have plans to use
Yes, implementation specific errors are applicable everywhere and it's the only common variant on all peripheral specific kinds to express problems caused by HAL implementations or drivers. If you can think of a better name, bikeshed away. 😅 |
Shouldn't it be handled by type state?
I have GenericError, but I'm not convinced. |
We have barely started using
Even written differently. |
Sure. We're kind of in a gridlock (again, sic!). For reference, I was trying to introduce
They do have a different purpose. Anything in the peripheral specific Error is, well, peripheral specific (although we could discuss if we actually want to have a separate Error type for SMBus), anything in |
No criticism there. I am trying to highlight that despite a small number of errors (so far), there is already a case of similar-but-actually-different error variants. When more variants are added, this may become more complicated.
Exactly because of this, I believe more similar-but-slightly-different error variants will be available in the future and error handling on the user side will become more confusing. I believe it would be better for everybody if operations only return their own error variants and use |
As mentioned before that's an absolute no-go for me: the only thing I deeply care about is the ability to handle common errors generically and |
I believe what most people need is to handle protocol-own errors, especially I2C's NACK, not underlying errors.
|
I don't. Quite a few people actually declared it to be a deal breaker not to have the ability to declare custom errors which is even a step further in that direction (and something I do not agree with but I'm willing to compromise on).
That is correct, I missed that in the initial proposal and added it after a ton of discussion and requests. So? |
I am one of the persons that thinks that forwarding underlying error information must be possible. I believe error types would be more flexible without attempting to represent every possible hardware failure situation inside each error type. This information can be transported separately with the approach from proposal 3.
So, standardisation of underlying errors was not the most pressing need in the community. |
I am aware. And again that is a compromise I'm willing to make.
If you can get to the underlying error, you can get to the underlying error no matter what the error value is. So there's absolutely no difference in flexibility.
Err, you certainly have a way of twisting words in your favour. 🙃 |
Please refrain from personal attacks. |
@dbrgn That's not an attack but an observation. A lot of stuff was added after the initial approach after a ton of discussion; I don't see why this specific addition has any lesser value than the downcasting which was suggested even later. |
As demonstrated by #181 and other issues there's a real world usability issue with the
Error
type of theResult
of operations being an associated type. An associated type means that a driver or user of an HAL impl is required to adapt to the custom Error type; while possible for applications this makes universal drivers impossible.In my estimation the main reason for this was probably caused by
embedded-hal
predating the improvements to Rust type and specificallyError
handling. Now we do have better possibilities and with the upcoming big 1.0 breakage it should be possible to also revamp the Error types without too much grief.Currently our trait definition e.g. for I2C looks as follows:
My proposal would be to instead provide a specific
Error
type such as:and then redefine all traits as:
The
#[non_exhaustive]
flag (available from Rust 1.40.0, cf. https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html) allows us to easily extend the reasons in the feature without being a breaking change because it requires implementations which care about the actual error to also have a wildcard match branch, which is a good idea anyway.By having the
Error
types defined inembeded-hal
we do some advantages:Dowsides:
The text was updated successfully, but these errors were encountered: