1) For error handling, I think anyhow and thiserror have been gaining popularity for applications and libraries, respectively. There's also eyre, a fork of anyhow with some more options apparently, and color-eyre, which is a variation of eyre for colorful backtraces. Long story short, yeah, there's a lot of crates.
2) I've heard good things about sqlx as an alternative to diesel that lets you write raw type-safe SQL into your code and get it checked at compile time. It has good async support too.
3) I would probably mention async-std as an alternative to tokio.
Thanks for mentioning SQLx. I’ve been writing a SQLite integration recently using rusqlite, which is great and does the job, but some of the SQLx features, like the checked SQL statements and being able to fetch directly into a struct, look like they are worth checking out.
> Long story short, yeah, there's a lot of crates.
And I'd add one more to the pile: my [SNAFU] crate. It actually does what the author says they do by hand:
> So, I just write the boilerplate and make my errors descriptive enough I don’t need a backtrace. When I want to get fancy I implement the built-in `Error` trait, which used to be kinda useless but is now more helpful. And in another five years it’ll still work just fine.
SNAFU is all about creating enums for each error case so that the hierarchy of types tells you exactly what and where the error came from. In my ideal usage, you don't need a stacktrace or even origin line number / file name because each specific combination of error is used only once.
The nesting of types within specific contexts gives what I like to call a "semantic backtrace"
Example usage:
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("Unable to read configuration from {}: {}", path.display(), source))]
ReadConfiguration { source: io::Error, path: PathBuf },
#[snafu(display("Unable to write configuration to {}: {}", path.display(), source))]
WriteConfiguration { source: io::Error, path: PathBuf },
}
type Result<T, E = Error> = std::result::Result<T, E>;
fn example(path: &Path) -> Result<()> {
let data = fs::read_to_string(path).context(ReadConfiguration { path })?;
fs::write(data, path).context(WriteConfiguration { path })?;
Ok(())
}
This creates the `Error` enum and implements `std::error::Error` for it, plus what I call context selectors to easily add on to fallible calls to help group them. Note that both SNAFU errors are caused by an `io::Error`, but the context in which the `io::Error` is created is vastly different.
> for applications and libraries, respectively
I've yet to understand why people want to draw a line between these two cases. For me, the only difference is that the error type in a library needs to be conscious that it's part of the public API. That's why SNAFU can generate [opaque] error types
---
Other bonuses:
- Doesn't require heap allocation (but can make of use it)
- Works in no-std environments
- Comes with extension traits for `Result` and `Option`, and feature flagged ones for `Future` and `Stream`
- Supports generic errors (types and lifetimes)
- Can create backtraces (either from the backtrace crate or the unstable `Backtrace` type in the standard library)
1) For error handling, I think anyhow and thiserror have been gaining popularity for applications and libraries, respectively. There's also eyre, a fork of anyhow with some more options apparently, and color-eyre, which is a variation of eyre for colorful backtraces. Long story short, yeah, there's a lot of crates.
2) I've heard good things about sqlx as an alternative to diesel that lets you write raw type-safe SQL into your code and get it checked at compile time. It has good async support too.
3) I would probably mention async-std as an alternative to tokio.