What is the state of the art approach to handling unrecoverable errors in Rust?


#1

Hi,

I have doubts about how should I handle unrecoverable errors in Rust. Let’s consider the following snippet:

let mut file = File::open(config_file).expect("Failed to open configuration file");

That’s my current approach. It has the drawback of being overly verbose.

thread 'main' panicked at 'Failed to open configuration file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', libcore/result.rs:945:5

One has to basically read the whole message all to find out what’s the problem. One would rather expect a more concise error message: (that’s actually a rustc error)

error: couldn't read "foo": No such file or directory (os error 2)

On the other hand, one could pattern match at every place where expect is used but it would make the code overly verbose and seemingly defy the purpose of expect. Some unwrap_or could or or_else could be possible but then we’re entering a hell of mismatching return types.

What’s the recommended approach, so that the code is concise and the user receives understandable error messages?

EDIT: The book already gives hints about error handling in library code. What I’m asking here about is error handling in application code. The snippet should rather read:

fn main() {
    ...
    let mut file = File::open(config_file).expect("Failed to open configuration file");
    ...
}

#2

To me what you demonstrate is buggy code. The function should be returning a Result. (Or if wanting the program to abort give a user orientated message.) unwrap, expect are assertions that developer views as valid.
Examples just use them out of laziness and to keep clarity.

Handling unrecoverable errors is down to each application/system.
A GUI could inform the user using panic hook.
Services should probably be logging the output and restarting.


#3

First off, I’d recommend reading the relevant chapters from The Book.

Not being able to read/open a file isn’t an unrecoverable error so you’d usually return a Result, most probably leveraging the power that the failure crate gives you. If you use failure you can create an “error chain” to add more context to the problem. This allows your main() function to emit a message along the lines of “we couldn’t start the application because we couldn’t load the config file because we couldn’t read the file because the file doesn’t exist”.

Unrecoverable errors are typically things like out-of-memory, the machine’s hard-drive just got yanked or other occasions where the application/world’s state is completely FUBAR. In this case the only realistic solution you have is to crash the program, possibly logging the cause for the crash.


#4

jonh, Michael-F-Bryan: this code is already in fn main(). There’s nowhere else to give the Result to. Maybe my snippet should have been clearer:

fn main() {
    ... 
    let mut file = File::open(config_file).expect("Failed to open configuration file");
    ...
}

The book covers error handling in library code. What I’m asking here about is error handling in application code.

I updated the OP to reflect this.


#5

There’s a template I use often when wanting to create a CLI app. It includes things like proper logging and command-line argument parsing as well as error handling, but the general idea is to have a run(Args, &Logger) -> Result<(), Error> function which executes your actual program and then main() is just a shell which parses the command-line arguments, configures the logger then executes run(), printing out the error chain if anything went wrong.

The template is aimed more at proper CLI applications than once-off prototyping, so it may seem a little big/verbose. Feel free to use it in your own code or let me know if it’s confusing :grin:


#6

The trick is not to put code in main other than error handling:


fn main() {
     if let Err(e) = run() {
        eprintln!("error: {}", e);
        std::process::exit(1);
     }
}

fn run() -> Result<(), Box<std::error::Error>> {
   File::open("etc.")?;
   …
}

#7

It’s also worth noting that you’ll be able to return Result from main in the future with pretty much the same effect!

In particular, returning Result<(), E: Debug> from main is going to be stable in 1.26 (the next version).


#8

This isn’t a lot better (but still) when it comes to the error messages, though: https://play.rust-lang.org/?gist=c6b6f05cd75413bff2aa998894e572c6&version=beta&mode=debug


#9

Ah, yeah… Oh well, nice for prototyping at least!


#10

The error in main is intended for examples and playground, so it uses developer-oriented Debug rather than user-friendly Display.

There are many ways to present errors to end users, and different developers have different opinions on how exactly that should be done, so Rust doesn’t default to any particular style. It’s left for you to implement in real programs.

Note that I/O errors in Rust never contain any filename information, so even if main or panic displayed it nicer, it would still be poor from end-user perspective. It’s a deliberate choice for performance because it makes Result smaller and avoids allocation. However, in practice you will want another crate like error_chain/quick_error/failure to use your custom error type with more context.


#11

What if you have an Option somewhere at the path?

https://play.rust-lang.org/?gist=6a5997ec15ab46783149790a664f8f7b&version=beta&mode=debug

The Box<Error> approach doesn’t play on well with failure :frowning:
https://play.rust-lang.org/?gist=2dc0d1f5b422851176ac3c3491e69c4b&version=beta&mode=debug

error[E0277]: the trait bound `failure::Context<&str>: std::error::Error` is not satisfied
  --> src/main.rs:41:16
   |
41 |     let path = env::current_dir().context("Failed to get current directory")?;
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::error::Error` is not implemented for `failure::Context<&str>`
   |
   = note: required because of the requirements on the impl of `std::convert::From<failure::Context<&str>>` for `std::boxed::Box<std::error::Error>`
   = note: required by `std::convert::From::from`


#12

Which of those three would you recommend?


#13

Forget Box<Error>, use failure::Error. Then you can still get backtraces, too.


#14

If you have Option, use .ok_or()?

Failure is a replacement for Box<Error>. Use types it recommends.


#15

What are the guidelines for choosing between unwrap or expect? I can’t see it in the book.


#17

Both are used for the same thing. Just expect allows the extra message. If you get used to using unimplemented, can also use as a string in expect; to give easy to find search results.

Note: Result and crates like failure are about error propagating through code. For polished UI custom error messages are better and leave the error detail to the side (in a log or extended info.)