-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Updated book with changes to library error handling.
A new section covers error handling with a couple of recommendations. All examples and existing sections are updated with the changes.
- Loading branch information
Showing
11 changed files
with
256 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# Error handling in Calloop | ||
|
||
## Super quick advice | ||
|
||
Use [*Thiserror*](https://crates.io/crates/thiserror) to create structured errors for your library, and [*Anyhow*](https://crates.io/crates/anyhow) to propagate and pass them across API boundaries, like in [the ZeroMQ example](ch04-06-the-full-zeromq-event-source-code.md). | ||
|
||
## Overview | ||
|
||
Most error handling crates/guides/documentation for Rust focus on one of two situations: | ||
|
||
- Creating errors that an API can propagate out to a user of the API, or | ||
- Making your library deal nicely with the `Result`s from closure or trait methods that it might call | ||
|
||
Calloop has to do both of these things. It needs to provide a library user with errors that work well with `?` and common error-handling idioms in their own code, and it needs to handle errors from the callbacks you give to `process_events()` or `insert_source()`. It *also* needs to provide some flexibility in the `EventSource` trait, which is used both for internal event sources and by users of the library. | ||
|
||
Because of this, error handling in Calloop leans more towards having separate error types for different concerns. This may mean that there is some extra conversion code in places like returning results from `process_events()`, or in callbacks that use other libraries. However, we try to make it smoother to do these conversions, and to make sure information isn't lost in doing so. | ||
|
||
The place where this becomes the most complex is in the `process_events()` method on the `EventSource` trait. | ||
|
||
## The Error type on the EventSource trait | ||
|
||
The `EventSource` trait contains an associated type named `Error`, which forms part of the return type from `process_events()`. This type must implement `std::error::Error` and be `Sync + Send`. | ||
|
||
As a rule, if you implement `EventSource` you should try to split your errors into two different categories: | ||
|
||
- Errors that make sense as a kind of event. These should be a part of the `Event` associated type eg. as an enum or `Result`. | ||
- Errors that mean your event source simply cannot process more events. These should form the `Error` associated type. | ||
|
||
For an example, take Calloop's channel type, [`calloop::channel::Channel`](api/calloop/channel/struct.Channel.html). When the sending end is dropped, no more messages can be received after that point. But this is not returned as an error when calling `process_events()`, because you still want to (and can!) receive messages sent before that point that might still be in the queue. Hence the events received by the callback for this source can be `Msg(e)` or `Closed`. | ||
|
||
However, if the internal ping source produces an error, there is no way for the sending end of the channel to notify the receiver. It is impossible to process more events on this event source, and the caller needs to decide how to recover from this situation. Hence this is returned as a `ChannelError` from `process_events()`. | ||
|
||
Another example might be an event source that represents a running subprocess. If the subprocess exits with a non-zero status code, or the executable can't be found, those don't mean that events can no longer be processed. They can be provided to the caller through the callback. But if the lower level sources being used to run (eg. an asynchronous executor or subprocess file descriptor) fail to work as expected, `process_events()` should return an error. | ||
|
||
If your crate already has some form of structured error handling, Calloop's error types should pose no problem to integrate into this. All of Calloop's errors implement `std::error::Error` and can be manipulated the same as any other error types. | ||
|
||
If you want a more flexible or general approach, and you're not sure where to start, here are some suggestions that might help. | ||
|
||
> Please note that in what follows, the name `Error` can refer to one of two different things: | ||
> - the trait `std::error::Error` - this will be whenever it qualifies a trait object ie. `dyn Error` means `dyn std::error::Error` | ||
> - the associated type `Error` on the `EventSource` trait ie. as `type Error = ...` | ||
### Thiserror and Anyhow | ||
|
||
[*Thiserror*](https://crates.io/crates/thiserror) and [*Anyhow*](https://crates.io/crates/anyhow) are two excellent error handling crates crated by David Tolnay. Thiserror provides procedural macros for creating structured error types with minimal runtime cost. Anyhow provides some extremely flexible ways to combine errors from different sources and propagate them. This is the approach used in [the ZeroMQ example](ch04-06-the-full-zeromq-event-source-code.md). | ||
|
||
One wrinkle in this approach is that `anyhow::Error` does not, in fact, implement `std::error::Error`. This means it can't directly be used as the associated type `calloop::EventSource::Error`. That's where Thiserror comes in. | ||
|
||
The basic idea is that you use Thiserror to create an error type to use as the associated type on your event source. This could be a single element struct like this: | ||
|
||
```rust,noplayground | ||
#[derive(thiserror::Error, Debug)] | ||
#[error(transparent)] | ||
pub struct MyError(#[from] anyhow::Error); | ||
``` | ||
|
||
This creates a minimal implementation for a struct that forwards the important `std::error::Error` trait methods to the encapsulated `anyhow::Error` value. (You could also use Thiserror to create an error with a specific variant for, and conversion from, `zmq::Error` if that's useful.) | ||
|
||
But how do we get from one of Calloop's errors (or a third party library's) to this "anyhow" value? One way is to use Anyhow's `context` trait method, which is implemented for any implementation of `std::error::Error`. This is doubly useful: it creates an `anyhow::Error` from the original error, and also adds a message that appears in the traceback. For example: | ||
|
||
```rust,noplayground | ||
self.socket | ||
.send_multipart(parts, 0) | ||
.context("Failed to send message")?; | ||
``` | ||
|
||
Here, the result of `send_multipart()` might be a `zmq::Error`, a type that is completely unrelated to Calloop. Calling `context()` wraps it in an `anyhow::Error` with the message *"Failed to send message"*, which will appear in a traceback if the error (or one containing it) is printed with `{:?}`. The `?` operator then converts it our own `MyError` type if it needs to return early. | ||
|
||
### Arc-wrapped errors | ||
|
||
Since any error can be converted to a `Box<dyn Error>`, this suggests another simple approach for error handling. Indeed it's pretty common to find libraries returning `Result<..., Box<dyn Error>>`. | ||
|
||
Unfortunately you cannot simply set `type Error = Box<dyn Error + Sync + Send>` in your event source. This is for the same reason as with Anyhow: `Box<dyn Error>` does not actually implement the `Error` trait. | ||
|
||
There is a smart pointer type in `std` that *does* allow this though: setting `type Error = std::sync::Arc<dyn Error + Sync + Send>` works fine. You can do this with the `map_err()` method on a `Result`: | ||
|
||
```rust,noplayground | ||
type Error = Arc<dyn Error + Sync + Send>; | ||
fn process_events<F>(...) -> Result<calloop::PostAction, Self::Error> where ... { | ||
self.nested_source | ||
.process_events(readiness, token, |_, _| {}) | ||
.map_err(|e| Arc::new(e) as Arc<dyn Error + Sync + Send>)?; | ||
} | ||
``` | ||
|
||
The `Arc::new(e) as ...` is known as an [unsized coercion](https://doc.rust-lang.org/reference/type-coercions.html#unsized-coercions). You can even just do: | ||
|
||
```rust,noplayground | ||
self.nested_source | ||
.process_events(readiness, token, |_, _| {}) | ||
.map_err(Box::from)?; | ||
``` | ||
|
||
...since the `?` takes care of the second step of the conversion (`Box` to `Arc` in this case). | ||
|
||
### Which to choose | ||
|
||
Arc-wrapping errors only really has the advantage of fewer 3rd-party dependencies, and whether that really is an advantage depends on context. If it's a matter of policy, or simply not needing anything more, use this approach. | ||
|
||
Anyhow and Thiserror are both extremely lean in terms of code size, performance and their own dependencies. The extra `context()` call is exactly the same number of lines of code as `map_err()` but has the advantage of providing more information. Using Thiserror also lowers the effort for more structured error handling in the future. If those seem useful to you, use this approach. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.