-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added book chapter on error handling and fixed any out-of-date inform…
…ation.
- Loading branch information
Showing
7 changed files
with
152 additions
and
31 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# 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`. | ||
|
||
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
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