Rust Error Handling

I have read and attempted to use several guides and ways to deal with errors in Rust.
However, I freely admit to getting somewhat lost in all of them about half way through, buried under all kinds of 'super' crates needed to 'process' them, special #[derive...] pragmas declarations (a horrible feature of the language anyway, imho), custom enum types defined with respect to those special crates, custom conversion implementations defined with respect to those special crates, etc, etc. None of it seems like constructive effort to me.

Worst of all, I do not get the purpose of it all. Why go into all this trouble, in the end just to be able to cascade the error up the chains of command with ? ? Why not use so disparaged panic! and do it simply and in one go?

I have written myself a little macro that will identify the exact function and place where the actual error occurred, so it makes it easy to find in potentially voluminous traces. I would argue easier than all this error forwarding does.

I find it hard to think of any meaningful examples of the mythical 'recoverable errors' that are supposed to justify it all. I guess I need to be persuaded to 'the cause' with concise arguments and examples. A simpler way to handle the errors would not go amiss either.

Why recoverable error, or error handling in general is important? You would not be happy if the browser crashes when you put invalid url on its address bar and type enter.

7 Likes

Just to play the devil's advocate, that may in fact be sometimes preferable to being subjected to a very very long wait while it searches the whole internet for some similar existing site in order to "recover" :slight_smile:

I get checking and reporting data input errors. Maybe even assuming some sensible values instead, and/or returning to a REPL loop, if that is how the program operates. However, all of that is best done immediately, printing informative warnings about the errors and the assumptions made to overcome them. I still do not see, even in these situations, how returning those errors all the way up, just to fail, helps anything.

I guess what I am asking is this: Why/how is it supposed to be better to pass the buck up about the recovery/correction/failure decisions? Surely, at the low level, where the error arose, we know most about it. Passing who knows where no matter how sophisticated 'error types' seems to me like a poor substitute.

The REPL is at the top, a number parsing function is at the bottom. If the REPL is to print an error message and ask the user to type something in again, the information that the parsing failed has to propagate from the integer parsing function up to the REPL.

The low level function doesn't necessarily know the most about how to handle the error. For example the str::parse function doesn't know whether it is being used inside a REPL, what error message to print to what device, and whether the user should try again.

11 Likes

In the context of a library, you generally want to give consumers of library a choice on what to do when errors are encountered. If nothing else, they may have different ways of reporting/logging errors in the end application. You usually don't want to use a library that brings your whole app down when there's an error. Thus, unrecoverable errors tend to bubble up to the API boundary.

In the case of a user-facing app and recoverable errors, you bubble the error up to the point where it makes sense to handle it. That is not always "immediately". Maybe my export module finds out it's going to have to overwrite a file, so it bubbles an error up containing the unwritten data to the UI layer, which asks the user to make a decision and continues from there.

In the case of a user-facing app and unrecoverable errors, sometimes bailing immediately is fine. However, there are still reasons to bubble up. A couple related ones are:

  • It's very rare to write user-friendly messages in the unhappy path inline, where as a check-and-abort point is all about the unhappy path; it's more natural to have a lot of code making it nice there
  • Those who do try to make nice messages at the point of failure end up with a lot of matches and the like to do the error handling, obscuring the happy path and making for much less streamlined code

In fact, he benefits of avoiding the latter are so great that even if I am writing a throw-away, could-have-been-a-script program, I'll use the ? machinery to avoid 20 .expect("couldn't open file") lines cluttering up my code. This also leads to a nicer maintenance experience if the throw-away graduates to still-using-it-6-months-later and I decide it's time to add proper error handling (nicer messaging, recover in some cases, whatever). It's a pain to make a call stack recoverable via refactoring after the fact.

In other words, it can be beneficial to the code base / programmer, even if you don't care about error handling and are just going to bail with the original error. If you just don't care about any of the other reasons, you can still reap the benefits by bubbling up. You can also help ensure minimal future effort by using something like the anyhow crate to "unify" all the standard library errors (or any other Error implementer), and to create your own errors without creating any custom types, etc.

Also, if you end up aborting instead of unwinding, your resources (unflushed files, etc) may not get cleaned up.

11 Likes

Just to pull out something that's implicit in what others have already written… it's quite easy and normal to go from error value → panic with unwrap, expect, etc. Going in the opposite direction, panic → None or Err(...), is much trickier. You can't do it at all in a program compiled with -C panic=abort (which is controlled by the end user of your code, not you), and even when unwinding is enabled it requires going through catch_unwind, which is pretty awkward and verbose to use. So if you think that any dependent of your code, direct or indirect, might ever want to handle your error as a normal value, you shouldn't make their life difficult by panicking. On the other hand, if you don't know how your dependents are going to handle your errors, you can give them an Err(...) with a clear conscience since they can easily turn this into a panic if they want.

I endorse quinedot's recommendation of anyhow for straightforward Result-based error handling.

4 Likes

You don't need any crates for that. ? works with any Result<T, E> and Result<T, F> as long as F: From<E>.

You may want to try a language without derives before making such an agressive assertion. If you are willing to bloat the code with repetitive and un-interesting implementation of traits like PartialOrd or Deserialize full of manually-written bugs, then go ahead, but don't expect that code to pass any review in the community.

8 Likes

You don't need any external crates, The Book covers errors in vanilla Rust very well. You may need extra tools to consume errors but not to create and/or propagate them. Result along with writing your own Error enum with variants for each category and some From impls is all you really need. The standard library has lots of Error and Result types for individual modules like fs and io. Those can be used as examples for your own Error and Result types.

1 Like

I have written myself a little macro that will identify the exact function and place where the actual error occurred, so it makes it easy to find in potentially voluminous traces. I would argue easier than all this error forwarding does.

I'd love to see this if you're willing to share. As a fairly inexperienced Rustacean, I too find "proper" error handling to be a significant headache when writing programs in Rust.

2 Likes

In the handful of libraries I've developed, I've never used any "error handling" crates, because most libraries never need to handle errors. Just report them to the caller.

The most extreme case I've written is content-security-policy[1], which doesn't even use Result for most of its errors (because the CSP standard allows you to configure the policy to warn-only, which means it can return Allow and a list of Violations at the same time, but also because I really wanted the code to match the spec, which silently discards unrecognized CSP rules and, therefore, can literally return a CspList for any arbitrary string).

In a sense, errors are a social construct. A file not existing can be an error if the application wants to read it, but in an application that is implementing a fallback list, it can just mean it needs to use the default, or create the file, or check another path. This remark by pcwalton made an impact on me when I read it:

Sometimes you want the error case to be fast. That's actually what everyone who is bringing up this argument is implicitly acknowledging by saying that you shouldn't use exceptions for all errors. In that case you need an alternative mechanism for error handling, which is readily provided by Rust's facilities. It is not true that Rust only cares about performance of errors and not ergonomics, and I don't know where that idea came from--that's why we have the try macro and unwrap!

Rust does follow the model of "exceptions for truly exceptional cases"--that's what panic is for. It's implemented with unwinding under the hood, just as in C++, and you can change it to abort if you want.

I guess it really depends on whether you're in library code or UI code. If your approach to "error handling," whatever that means, is to bail out and report it to the user, then the error handling code is tied to your UI code. That might be println!, structured logging, or a GUI. The less a piece of code prescribes a particular UI, the more reusable it is.

If you're trying to write code that can be reused in multiple contexts, like a library or an application with a pluggable UI, you're going to need to make the error available to the UI code in a way that allows it to take care of presenting the failure to the user. If the UI is localized and you're writing a library, then reporting the error as a string won't be enough either, assuming you don't want to tie your library to a particular localization framework. Because it's going to be up to the application to translate that crash message (not just localizing it, but also "translating" it from the technical domain of the library into the user domain where "files" become "documents" and "database rows" become "accounts").

If your code is already tied to the UI, and you don't want to bubble the error up, then don't. You know what you're doing. YAGNI, and if you eventually do need it, you'll also have enough context to do it right.


  1. Warning: this code is actually pretty bad, and outdated. I've started rearchitecting parts of it to be more modern, less hacky, and better documented. But the error handling approach is probably not going to change very much, because its mostly driven by the way CSP itself works. ↩︎

4 Likes

The macro here!() is in my crate indxvec. It is great for panicking but can be added to just about any error message strings. For example, while using anyhow. I got inspired by some code like that, unfortunately I can not now remember where I first found it. I adapted it and added the mystery strings that will print it in bold red on linux terminals, to make the error even easier to find. Never tested it on Windows. If it gives you trouble on strange terminals, just remove those literal strings.

Note that you cannot simply pass the error to somewhere and then call the file!(), line!() macros, as they capture the code location where they are called from, which would no longer be where the error actually occurred.

Here is another 'aggressive' comment: it seems that in Rust, to get any really useful jobs done, you have to resort to macros. The best known libraries and crates bear silent witness to that. I have been trying to avoid that, with the exception of this one macro but I must say that sometimes it is an uphill struggle, being locked in the strict typing hell. I have now gone through all the options of generics and even enum based generics but none of them are easy. Particularly if you are primarily dealing with Vecs.

Explanation to others here: as you may have guessed, at fairly early stages of development, I am more concerned about my own coding errors, rather than those rather different 'recoverable errors' caused by the outside environment or abuse of the code by the users. For development purposes, panicking does seem like the best option.

1 Like

Macros are part of the language, you should learn them and use them. Of course, they have a higher cognitive cost than normal functions, so you should do it with prudence. Correct, ergonomic macros are hard to write, and even harder to read and debug. They also often cause problems for IDEs and other static analysis tools. You should carefully weight whether a macro really pulls its weight. If you write one, always document it.

A simple macro without recursion or many arms is usually fine, though. I commonly use one-off macros in my code for simple repetitive tasks, like writing tests for related types or some simple impls for primitive arithmetic types.

3 Likes

Yes, I will try that. Simple arithmetic types in simple Vecs seem a lot more difficult to deal with generically than they ought to be.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.