Hi everybody,
I was meant to write a blog post for the #Rust2018, but my blog is basically blocked because of the issue I am going to write about, so… I hope you don’t mind I expose my half-planned idea here.
I saw the discussion around the catch Pre-RFC, and, together with the failure crate and the previous attempts (error-chain, for example) I appreciate a lot of people are trying to fix the problems we have with Error management, and I am extremely grateful to everyone who is trying.
But I think the solution can only be found in the compiler, as crates are too limited in this, especially in the stable channel.
The problem is that, in Rust, creating errors is quite easy, passing them around is hard, and catching them is nearly impossible, at least in my experience.
I am the author of strange, an almost unknown static website generator, and to write it I had to integrate different crates, namely pulldown, tera, handlebars, serde, hyper among others.
Almost all of them (plus std::io) have a different way of creating errors, and of course I want to add some debugging context to them.
The best solution was error-chain. It was as simple as defining a lot of foreign_links{}
and .chain_err()
every Result
.
This made the compiler happy, as all my Result<Something>
were returning the same error type: error-chain’s.
I tried to convert the codebase to failure and… failed. For over a week.
This made me think that what error-chain is doing should be the std::Error
object (trait, struct, enum, whatever it takes. Probably a combination of all three) in the core. Create a special case if needed, but:
- all the errors should be automatically converted into the core Error. No need for
.into()
or?
- they should be chainable (have a context that in turn can have a context, etc.)
- they should support optional backtraces
- they should be matchable
Failure is getting there, but adding a context is a real pain, and wrapping other errors is almost impossible. You need a lot of boilerplate.
An example of what I wish:
function read_config(f: &str) -> Result<Config, Error> {
file::open(f)
.and_then(|f| BufReader::new(f).read_to_end())
.and_then(Config::from_json)
// .error_context(|| format!("Error loading {}", f) // optional
}
(look, ma’, no question marks! )
Basically I don’t care if I am returning an io::Error, a config::Error, a serde::Error or whatever.
When I use it, I would like to do
match read_config(f) {
Err(io::Error(msg)) => panic!("Your disk is broken!"),
Err(json::Error(msg)) => panic!("Your json is broken!"),
Err(config::Error(msg)) => panic!("Your config parser is broken!"),
Err(_) => panic!("Everything is broken!"),
Ok(c) => panic!("No panic!")
}
And, naturally, print the causes like in the standard dump_error()
function error-chain suggests.
Obviously, the errors should impl std::Error
to be automatically converted, and the std::Error
becomes an automatically-growing enum that can be matched against all errors that impl
it.
It’s only half of an idea, but I hope you get the point.
As I said, the situation in the crates is already pretty bleak, as everyone implements a different error crate, and it’s going to get worse, with people not upgrading, eitheri because they can’t or unable (like me), and an official solution would at least solve the problem in a single way for the future, instead of leaving people to choose different paths.
I apologise if this came out too similar to a rant, but this is probably the most frustrating thing I found in Rust until now.
Thanks for reading.