-
Notifications
You must be signed in to change notification settings - Fork 313
Description
I realize opinions might run a bit strong here, but I'd like to make a case against the use of error-chain
(or failure
) for error handling in the cookbook examples. I think the error handling mechanisms provided by the standard library should be used instead.
Some background: I've been using Rust for a long time (since before 1.0), so the cookbook isn't actually something I've ever read in close detail before. I've also never used error-chain
before (which I think probably reveals some bias in my argument here, FWIW).
I have an experience report from a colleague of mine that is in the process of learning Rust. They were using this cookbook as a way of reading example code. One of the things they did in their own code was copy the use of Result<()>
. But their code wouldn't compile and they couldn't figure out why. When they asked me, I said, "oh, you're probably missing a type Result<T> = ...;
somewhere in your code." But then they pointed me to the example in which they saw Result<()>
, which does not have any such type alias.
It took me at least a few minutes of perplexed digging before I was actually able to figure out how that code was compiling. It turns out that the error_chain!
macro is defining a Result
type alias for you. Personally, I think this makes learning from these examples much harder than necessary. Some might even argue that the Result
type alias itself is problematic, but even aside from that argument, I think having it implicitly defined and brought into scope is really quite surprising, especially for beginners that might be trying to learn Rust through this cookbook.
Of course, this is just one experience report. In any case, what are the alternatives? Here are some that I can think of:
- Keep
error-chain
, but don't use theResult
type alias. (What does one use instead that doesn't suffer from the same implicitness fault?) - Switch to using
failure
. - Switch to using only the facilities provided by the standard library.
- Remove proper error handling altogether and use
unwrap
/panic
.
I don't think we should remove proper error handling. I think a cookbook that shows how to do error propagation is much more valuable than a cookbook that doesn't. And I think such examples are incredibly useful for beginners.
I don't think we should use failure
because its development is a bit in flux, despite it being used in a fair number of crates. In particular, there is a merged RFC to fix the standard library's Error
trait, and this will likely have an impact on failure
's API. So I think there is a collective "let's wait and see" on that front. However, if and when failure
's future becomes solidified, it might be a good idea to transition to using it.
I also don't think we should use error-chain
either, even if we were able to fix the implicitness problem in the examples today. Of course, fixing the implicitness would be something that I'd see as an improvement! For the most part, I perceive error-chain
as something that the ecosystem is probably not going to adopt. I think the direction is likely headed more towards failure
, or perhaps, even std
itself.
Ultimately, my argument concludes here that we should just use what the standard library provides. In particular, if I were to write the aforementioned example:
#[macro_use]
extern crate error_chain;
use std::fs::File;
use std::io::{Write, BufReader, BufRead};
error_chain! {
foreign_links {
Io(std::io::Error);
}
}
fn run() -> Result<()> {
let path = "lines.txt";
let mut output = File::create(path)?;
write!(output, "Rust\n💖\nFun")?;
let input = File::open(path)?;
let buffered = BufReader::new(input);
for line in buffered.lines() {
println!("{}", line?);
}
Ok(())
}
quick_main!(run);
then I would write it like this:
use std::fs::File;
use std::io::{self, Write, BufReader, BufRead};
use std::process;
fn run() -> io::Result<()> {
let path = "lines.txt";
let mut output = File::create(path)?;
write!(output, "Rust\n💖\nFun")?;
let input = File::open(path)?;
let buffered = BufReader::new(input);
for line in buffered.lines() {
println!("{}", line?);
}
Ok(())
}
fn main() {
if let Err(err) = run() {
eprintln!("{}", err);
process::exit(1);
}
}
If an example requires dealing with multiple errors, then I'd either write out a new error type, possibly with appropriate From
impls, or just use Box<Error>
. Namely, Box<Error>
is used in command line tools effectively as a means of bubbling an error up to main
. The downside to using Box<Error>
is that it's probably not something one wants to use in a library.
I also believe that the examples should not return Result
directly from main
, as suggested in #433. I explained my position on that front in more detail here. With that said, I personally feel less strongly on this point than I do about using error_chain
or failure
.
Finally, I certainly don't intend to say that error-chain
(or failure
) should be expunged from the cookbook entirely. I just think they shouldn't be the go-to error handling mechanism for every single example.
Thanks for your consideration! And thanks so much for all your hard work on this wonderful resource. I'd like to end on a positive note: my colleague reports that the cookbook has been a wonderful resource for learning Rust. In particular, he found that the cookbook---above other docs---made it much easier to learn how to actually start writing a full program. That's an awesome compliment!