11
$\begingroup$

Rust has two popular packages for augmenting its error handling:

  • anyhow, for libraries, created by David Tolnay.

  • thiserror, for applications, created by David Tolnay.

They control how errors are created, annotated, and handled.

Why is this distinction needed? Should it be a consideration when designing a language?

$\endgroup$

4 Answers 4

15
$\begingroup$

Libraries require higher "quality" error types

The focus of the library (thiserror)

Libraries are providers of error information.

Libraries tend to use structured errors, such as enums, for subsequent pattern matching processing.

The library cares about the necessary information about errors, and the smaller the overall structure, the better.

It would be great if macros could be provided to simplify this process.

The focus of the application (anyhow)

Application are consumers of error information.

The application generally consumes errors directly, and the application does not care about the structure of the error, as long as it can generate detailed error information.

The application doesn't care how to recover from the error, generally it's because the user didn't provide enough information.

The user should supplement the missing information according to the error message and restart the whole process.

Implications for language design

Providers need mechanisms like struct + derive macros, while consumers need mechanisms like Error trait + bail/throw/raise macros.

$\endgroup$
6
$\begingroup$

Not a Rust expert, but I'll answer this from the perspective of a programmer and as a language designer.

Applications

Applications are in a special position: they know whether an error must be handled and how delicately.

Sometimes, applications can just quit on error. I wrote a command-line tool that just printed an error on malloc() failure and exited. That was fine.

Sometimes, applications need to be super careful. I'm sure systemd has to be careful while in the middle of bringing services up.

Libraries

Libraries know the same thing that applications do, but only for themselves. There may be other libraries and an application in the same process space.

So a library must handle its own errors properly, but it also must not remove the choice of the application.

IIRC, libgmp, an arbitrary-precision number library, would quit on allocation failure, and that made application authors very sad because their application would suddenly quit without warning, and they couldn't do anything about it short of patching the library.

I have implemented a library myself, and this library does not just quit on allocation failure; it returns the failure.

Language Design

As we have seen above, libraries and applications have different concerns regarding error handling.

I believe this is why Rust has those two libraries.

But such error handling should not be built-in. That is a bad design.

Instead, the language should be designed in such a way that the error handling can be implemented as a library. This is hard because the language designer often can't anticipate how the community will want to handle errors, but I think the recent expressivity question and David Young's answer provide a great guideline because error handling is a global property of a program that we want to handle as locally as possible.

Essentially, a language that can do this is one that gives a lot of expressive power to libraries.

The fact that Rust has both implemented as libraries is a great testament to how well it was designed in this aspect.

$\endgroup$
1
  • 1
    $\begingroup$ A further difficulty in error handling is information gathering. The problem there is that an application wants as lightweight as possible a treatment for a failure it expects (and handles), and as a thorough as possible a treatment (backtrace, ...) for a failure it didn't expect and needs to report to the user. How is a library author supposed to handle that? Anyone's guess... $\endgroup$ Jul 7 at 7:01
5
$\begingroup$

People who write applications don't often care whether they have strongly typed errors or not, given that that doesn't influence program behavior. Whether my error is stringly typed or strongly typed, it has no effect on the output, even though ideologically the latter is often better because of the semantic distinctions. For cases like this, anyhow is appropriate, because it allows you to layer stringly-typed context onto errors without much verbosity. thiserror, on the other hand, has support for strongly-typed errors. A user of a library may wish to distinguish based on the type of error in ways the library author cannot anticipate, so having errors be typed works well (because they can be pattern matched on).

I would, however, like to note that the pattern of a single project-wide error enum largely gets rid of the benefits of thiserror-style strongly-typed errors. If a function is doing I/O in multiple places, and both return a crate::Error::Io(std::io::Error), that is entirely unhelpful for a user to actually distinguish what the error is. For this, I suggest taking a read of https://sabrinajewson.org/blog/errors, a wonderful article on error locality and adding context to error messages. It's verbose (although, using thiserror or a similar macro for this makes it less so), but I think working towards a concise way of using the patterns shown here is critical to having really good error handling infrastructure.

$\endgroup$
3
$\begingroup$

Errors are problems that [thing] decides not to solve. Instead [thing] passes the buck. You need to think about who you are passing the buck to.

If [thing] is a library, it is passing the buck to code. It should make the errors easy for code to use. Error numbers are one example of this, and solve the most basic problem when dealing with errors in code: comparability (404 == 404). Another nice property is backwards compatibility. If errors get more specific in the future, you don't want comparisons to break (see Go's errors.Is()). Yet another nice property is being able to track, through many layers of function calls, which errors are handled and which ones aren't (ex: Java's throws).

If [thing] is an application, it is passing the buck to a human. It should make the errors easy to read. This usually means including as much context as possible. As we add more context to an error, and therefore more information, the question of how to structure it for comparability, compatibility, and safety become more complex.

You can absolutely do both, it's just slow. You have to spend time thinking about which things should be testable together so calling code doesn't have to deal with 100s of similar but not quite identical errors. You have to make sure you are adding enough information so that the user knows exactly what failed. When you decide an error needs more information attached you have to think about how the change would affect existing code. You have to make the information in the error easy for a machine or a human to parse (which usually means writing formatting functions for the humans). In practice, if you make error handling too onerous, programmers won't do it outside of a few this-100%-must-never-fail cases.

$\endgroup$
0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .