Error handling Isn't All About Errors
This is a modal window.
The media could not be loaded, either because the server or network failed or because the format is not supported.
Formal Metadata
Title |
| |
Title of Series | ||
Number of Parts | 10 | |
Author | ||
License | CC Attribution 3.0 Unported: You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor. | |
Identifiers | 10.5446/51902 (DOI) | |
Publisher | ||
Release Date | ||
Language |
Content Metadata
Subject Area | ||
Genre | ||
Abstract |
|
RustConf 202010 / 10
3
00:00
RhombusComputer fontInternetworkingJSONXMLComputer animation
00:41
Chemical equationFrame problemCurve fittingLevel (video gaming)Context awarenessTraffic reportingElectronic mailing listProjective planeException handlingLibrary (computing)Graph coloringFile formatHookingTheoryLatent heatClosed setProcess (computing)Traffic reportingConnectivity (graph theory)Dependent and independent variablesFuzzy logicConstructor (object-oriented programming)Sheaf (mathematics)ImplementationAdditionSingle-precision floating-point formatFunction (mathematics)Shape (magazine)Filter <Stochastik>Computer fileReading (process)CASE <Informatik>Mainframe computerConfiguration spaceContext awarenessType theoryMereologyOpen sourceMotion captureFrame problemWordBitFactory (trading post)Fundamental theorem of algebraState diagramConsistency
04:31
Connected spaceException handlingComputer fileIdentical particlesSubject indexingSerializabilitySoftware bugResultantComputer programmingThread (computing)Formal languageSocial classBuffer overflowIntegerEndliche ModelltheorieBound stateComputer animation
05:23
Traffic reportingType theoryRepresentation (politics)Source codeTelephone number mappingString (computer science)Electronic visual displaySerializabilityLibrary (computing)Heegaard splittingMereologyPoint (geometry)Formal languageTraffic reportingInformationAttribute grammarOperator (mathematics)Descriptive statisticsMacro (computer science)Boilerplate (text)String (computer science)Electronic visual displayType theoryContext awarenessRepresentation (politics)Variable (mathematics)Source codeFile formatInterface (computing)Matching (graph theory)Multiplication signRule of inferenceCuboidBitDrop (liquid)ResultantState diagramEndliche ModelltheorieData conversionComputer-assisted translationCombinational logicComputer configurationSheaf (mathematics)Graph coloringCodeFunction (mathematics)CompilerParameter (computer programming)Exception handlingAdditionFunctional (mathematics)Interior (topology)Enumerated typeDomain nameChainFlow separationNetwork topologyParsingRadical (chemistry)Letterpress printingDependent and independent variablesImplementationPropagatorMessage passingTelephone number mappingSoftware bugMemory managementMathematicsLevel (video gaming)Complex (psychology)Equivalence relationAuthorizationCartesian coordinate systemOrder (biology)Default (computer science)outputInheritance (object-oriented programming)Revision controlBlock (periodic table)Different (Kate Ryan album)Open sourceRow (database)Bound stateSubject indexingForm (programming)Generic programmingTracing (software)Uniform resource locatorHacker (term)Error messageHookingEmailReal numberPropagation of uncertaintyStability theoryFigurate numberFitness functionPlanningOpen setProcess (computing)Ferry CorstenAbstractionObject (grammar)Fiber bundleActive contour modelNumberStack (abstract data type)Gauß-FehlerintegralBlogHand fanFilm editingThumbnailSound effectProcedural programmingComputer programmingSpacetimeArrow of timeLine (geometry)ParsingMotion captureForcing (mathematics)
Transcript: English(auto-generated)
00:16
Hello, and welcome to my talk, error handling isn't all about errors.
00:21
Let me start by introducing myself. My name is Jane Lesby, and my pronouns are she-do. On the internet, I go by ya or ya-see, and I've been writing Rust for about 2.5 years now, though I was only recently hired to do so professionally by the Zcash Foundation. Now, quick shameless plug, I also maintain Awesome Rust Mentors.
00:42
Awesome Rust Mentors is a list of projects and people who are willing to provide mentorship to anyone who asks. If you're interested in finding a mentor, finding a project to get involved in, being a mentor, or getting people involved in your project, you should check it out. Also, to be clear, they are more than just these two mentors, so please check out the website.
01:01
Moving on. So, why error handling? I actually got into error handling on accident. It started as a yakshave when I wanted to open source a library that I wrote for work, but I wasn't happy with the error handling, so I decided to fix it up first. That yakshave ended with me writing error. Error is a fork of anyhow with support for customized error reports via a global
01:23
hook, similar to a panic hook. I also ended up writing color error. Color error is a library which provides a custom panic report hook and a custom error report hook for error. With these libraries, I'm now able to construct error reports that look like this. Here we can see the basic usage example.
01:42
In it, we have an error section, followed by a span trace section, and if you're not familiar with tracing, this is an extremely cool backtrace-like type of tracing spans. Then after that, we have a suggestion section, followed finally by an emitted backtrace section.
02:00
I can also enable backtrace capture, so here we now have a backtrace section, and we can control the formatting of this backtrace section. Here we've just pretty printed it in the style of color backtrace, which hides the relevant frames, in this case it's hidden 5 frames before read file, and 10 frames after main. We can also take this further though, by applying custom filters.
02:22
Here you can see we've got 11 frames hidden after read config, instead of 10 frames hidden after main. Because I've configured it to hide the mainframe, we can apply this custom filtering consistently to all of our error reports. So here you can see we have the same report format for panics as we did for our errors.
02:41
And with our error report hook, we can also bundle arbitrary data with our error reports. We can use this to implement things like custom sections in our error reports. Here you can see that in addition to the error section, we also have a command section, showing which command we tried to run, and a std error section, showing the output of the command when it failed.
03:01
We'll dig into this example more later, so please look forward to that. Finally though, we can also add errors as sections, which we can use to aggregate multiple errors into a single report, and format them all consistently. Now I'm not giving this talk to talk about error or color error. I'm giving this talk to share what I learned in that yak shave to fix the error handling
03:22
in my library, and how the process has changed how I look at error handling and error reporting. So what is error handling? Error handling is a lot of things when you zoom in up close. Error handling is defining errors. It's propagating errors and gathering context. And by context, I mean stuff like the path you tried to open, or backtrace showing
03:44
where your error came from. It's also reacting to specific errors, and discarding errors, doing so intentionally and visibly. Finally, it's also reporting errors and their gathered context. Now, this breakdown gets to the goal of my talk.
04:01
I have a theory that error handling is made more confusing by people trying to simplify it, because among other things, error handling is kind of annoying. I worry the fuzziness between the different responsibilities makes it hard for people to infer what tools they should be using when handling errors. And my hope is that by breaking error handling into its component parts, we can make it
04:21
easier to understand and explain. So let's start with the fundamentals. Note that this first bit was taken almost word for word from the Russ Spokes chapter on error handling. The Russ Spokes model for errors distinguishes between two classes of errors, recoverable and non-recoverable errors. Recoverable errors are errors that can reasonably be reacted to or reported when encountered.
04:45
These are errors like file not found or connection closed. Non-recoverable errors are errors that cannot reasonably be reacted to, only reported before exiting the thread or program. These errors are usually caused by bugs such as index out of bounds or integer overflow.
05:03
Now, most languages don't distinguish between these kinds of errors. For example, C++ has historically used exceptions for both. But Rust doesn't have exceptions. Instead, Rust has panic for non-recoverable errors and result for recoverable errors.
05:23
So panic. Non-recoverable errors in Rust are created via the panic macro. Here we can see an example of an index out of bounds error. The only input for the panic macro is an error message and optionally some context to include in that error message. Reporting and default context gathering is done by the panic hook.
05:43
And by default context gathering, I mean capturing the caller location or capturing the backtrace if it's enabled. And once that panic hook is done printing the report, the panic handler takes over and cleans up either by unwinding the thread stack or boarding the application altogether. Next, we have result.
06:03
For recoverable errors in Rust, we modeled them with the enum result TE. This enum has two variants, OK, which contains the value of an operation when it completes successfully, and error, which contains the error value of an operation when it could not be completed successfully.
06:20
We use result to combine two return types into one and assign meaning to each possibility. In addition to this, Rust has marked the result enum as must use, which makes the compiler emit a warning whenever the result is discarded implicitly, prompting us to discard it explicitly or otherwise handle it.
06:40
This helps us avoid ignoring errors accidentally and makes discarded errors visible to later readers. The big advantage of using enums for recoverable errors is that we must react to all errors. Here you can see we have to use match, or anything equivalent to match, to access the values inside of either variant.
07:03
With an enum, we cannot access the inner values without first accounting for all the variants that enum could possibly be. Next, we have the try trait and the try operator. The currently unstable try trait is used to model fallible return types in Rust. Indeed, result is a type that implements the try trait, as does option and some other
07:24
combinations thereof. With the try trait, Rust is able to abstract the propagation of errors with the try operator. Here we can see two equivalent code snippets. The first manually propagates the error using match and return. The second does the same by simply using the try operator to propagate the error.
07:43
Finally, we have the error trait. The error trait fills three roles in Rust. First, it lets us represent an open set of errors by converting any type that implements the error trait into an error trait object. This is important for composing errors, and it is what lets us expose our source errors
08:02
via the error trait regardless of their actual type. Second, the error trait lets us react to those specific errors by trying to downcast them back to their original type, safely. Rather than using match as we would with enums. Finally, the error trait provides an interface for reporters.
08:21
Now I'll get to this last bit some more in a minute, but first let's review. So by now we've covered fundamentals, and you know all the tools, the language, and the standard library gives you to work with the different kinds of errors. So let's see how these fit into our original breakdown of the parts of error handling, starting with recoverable errors.
08:41
So we define recoverable errors with types and traits, propagate them with the try operator. For matching and reacting, we do that with match or downcast depending on how it's represented. We discard them with drop or unwrap if we want to promote a recoverable error to
09:01
an unrecoverable one, and for reporting we use the error trait. For non-recoverable errors, we define them with panic macro, we propagate them, well we don't, it's built into the language actually, so you don't have to worry about it. For matching and reacting, please don't. This is the whole point of this split between recoverable and non-recoverable errors, like
09:23
you don't do this one thing for non-recoverable errors. For discarding, you can use catch-unwind, though I advise you to use caution before reaching for this. Finally, for reporting, we use the panic hook. Okay, so now we know all the fundamental tools built into the language and where they
09:45
fit into error handling. Next I'd like to dig into the more complex ways you can compose these tools to write some fancy error reporting, but first let's set straight some terminology. In the context of error reporting, an error is a description of why an operation failed,
10:01
whereas context is any information relevant to an error or an error report that is not itself an error, and an error report is the printed representation of an error in all of its associated contexts. Now the concept of error reports in error reporters isn't a concept that is common in the Rust ecosystem today, or any language's error handling ecosystem as far as I know.
10:25
However, it is a vocabulary that I find particularly compelling in the context of Rust error reporting, and this is largely because of how Rust has defined its error trait. Here's a simplified version of the error trait. Here you can see we've got two super traits, debug and display, which we must implement
10:44
to implement the error trait. Then below that we have two functions, source and backtrace, both with default implementations that we can override when needed. Next let's look at a simple error. Here we've got a type with no members, we derive debug on it, and then we implement
11:01
display where we just write our error message, and finally we just implement the error trait. Now we don't have a source or a backtrace, so we don't need to override any functions here, which is why the block on the trait implementation is empty. If we did have a source though, we would need to override source function in order
11:20
to return a reference to our source as an error trait object. Finally, let's take a look at a simple error reporter. Here we've implemented our error reporter as a short free function. This function takes an error and then prints that error and all of its sources, followed by a backtrace if our error captured one.
11:41
A more complex error reporter might also try to check all errors for a backtrace, or if it were a type and it was storing its own context in addition to the error, it might print that as well. Now in other languages, there is no distinction between errors and reporters, and this is largely due to a lack of the equivalent of the error trait.
12:03
The error trait equivalent in other languages is often quite simple, just a single function to grab the error message. These interfaces force you to either include your source error, your backtrace, and any other information you care about in your error message, or to avoid using the provided interface altogether. In Rust, we don't have to combine our messages all into one, in fact you're encouraged
12:24
not to. Including a source's error message in your display implementation and returning it as your source via the error trait is essentially a bug as it forces reporters to duplicate information when they print out the chain of error messages. By separating the source and the error message, we move responsibility of formatting away
12:43
from the errors themselves, making it possible to get fancy. In Rust, we can have the same error print to a log as one line, but to the terminal as many. This wouldn't be possible if the error trait didn't separate error message from other contexts such as the source error.
13:00
However, despite the fact that the error trait in Rust is more flexible than most other languages, it is still restrictive in some ways. The error trait can only represent errors with a single source. If you've ever written a parser, you might have run into this one, where you have multiple syntax errors at once. The error trait can only represent a chain of errors as a singly linked list, and so
13:23
it doesn't work well for domains like parsers where error causes end up looking more like a tree. In addition, you can only access three forms of context via the error trait. The error message, the source, and the backtrace. We can't return types like span trace or location without using hacks based on downcast
13:43
to work around the error trait. This prevents us from having things like error return traces. Now, I dream of a Rust where we can access things like the location an error was constructed, HTTP status pose, custom report sections, and more. If we could access these generic forms of context in error reporters, we could implement
14:02
error return traces generically and much more. Now, I do have plans for how we can fix these problems though, so in the future, this may no longer be an issue. Okay, so I think y'all have a vague idea of what an error reporter is now, so let's dig into an example on how to use one by recreating the custom section example from the beginning
14:24
of the talk. So, we're going to create a customized version of the command output function with a nicer to use interface. Instead of returning an output type with the stood error and stood out as vex of bytes and exit status, let's just return a string for stood out if the command succeeds,
14:40
and return an error report if the command fails. So, we start by implementing a trait. We have to do this because we can't implement methods and foreign types. Then, we're going to implement this trait for stood process command, and the first thing we're going to do is call the original output function. Then, we're going to convert the standard output to a string, and if the output was
15:02
a success, we are going to return that string. Otherwise, we're going to return an error indicating what went wrong. So, let's run this and see what happens. Error. Command exited unsuccessfully. Cool, we got our error report, but also not very helpful. I didn't even show you the main function or tell you what command I was running, so
15:22
let's figure that out next. So, first we're going to format the command as a string, and then we're going to shove that into the error report as a section with an added header. Now, these functions section and header are provided by color error, and they all just work with any types that implement display.
15:41
So, let's see how this changes things. Error. Command exited unsuccessfully. Command. Get cat. Okay, so we can see why the command failed. Cat isn't a real git command. I wish it was, but it's not. Now, this error isn't very descriptive. Sure, it describes what went wrong, but it's far too generic. So, let's go ahead and find a new error message with a more descriptive, or let's
16:03
just go ahead and find a new error with a more descriptive error message to wrap our source error. So, here's our main function where we call color error install, which just sets up our panic and our error hooks, and then we create a command to run git cat and use our output to function to run it. To wrap the error this returns, we're going to call this wrap error function, which is
16:23
provided by error. This function takes a result and an argument that implements the display trait, and if the result is the error variant, it creates a new error using the arg as the error message and the old error as the source, and it then returns this new error instead
16:41
of the original one as an error report. So, let's go ahead and run this, and we can see error. The cat could not be got because the command exited unsuccessfully and the command was git cat. Cool, this is pretty helpful. It's not quite there yet. We're still missing the std error output that the original example had, so let's
17:01
go ahead and add that finally. So, we're going to convert std error to a string just like we did with std out, and here we're going to show both std out and std error as sections into the final error report. Let's go ahead and see what we get. Error cat could not be got, command exited unsuccessfully, command was git cat, and
17:21
cat isn't a git command. Here's the suggestions for what we could run instead. Perfect, we finally have an error report including all the information we need. With it, we can pinpoint what went wrong, why it went wrong, and as an added bonus, how we can fix it. So, hopefully this example makes it a bit more clear how beneficial just a little context can be for error reports and help you understand why I think error
17:43
reporting is such an important concept. This is just a small example of what error can do. This is just what I've configured the hooks for color error to support. You can do much more if you take the time to write your own hooks, though I don't have the time to go into details on how to customize error as part of this talk,
18:00
so if you're interested in learning how to customize error, please check out my blog. Okay, so before we go on, I've got some tips related to error reporters that I think are useful to know. So, first of all, error reporters almost always implement from E for all error types. Now, this is because error reporters exist to generically format any error, and they're
18:22
built on the error trait, so it makes sense that we'd want an interface to create one from any error that implements the error trait. But as a consequence of this, error reporters cannot implement the error trait themselves. This is true for all types that implement from E for all errors, including anyhow error, error report, and box dyne error.
18:43
Implementing both of these traits ends up violating the overlap rule in that there would be two from implements to choose from when converting a reporter to itself. So, thanks to the fact that they can't implement the error trait, they also don't compose very well with other errors.
19:01
This is just one of the many reasons reporters aren't great to use in library APIs, because your users will have to jump through extra hoops to use them as sources for other errors, and they might not understand why. So, by now, you should know all the tools built into the language, how they fit into the various pieces of error handling, and have an understanding of how they can be
19:21
combined to write error reports. Finally, I'd like to take a look at the ecosystem at large to see what open source libraries we can use to help us with our five parts of error handling. So, quick reminder, here's our breakdown. Now, I'm going to introduce these libraries by how they fit into the error handling breakdown. Not every part will have a library to help, and some will be disproportionately
19:42
represented. So, I'd first like to start with this error for defining errors. This error is an error-derived macro, and it exists to reduce boilerplate by implementing commonly used traits for you, such as error and display and front. To use it, we start by adding the macro's identifier to our drive attribute.
20:02
Next, we can implement display with this error via error attributes. We can have a simple attribute, which contains only a string literal, and we can also include variables in our error messages with this error's custom format syntax for both unnamed and named members. This error can also handle implementing our source and backtrace functions for us.
20:24
Any member with the name source or backtrace will automatically be returned from the appropriate function, or this can also be done explicitly using the source attribute. This error can also implement from with the from attribute, and note that this also implies the source attribute when used.
20:42
This isn't all this error can do, so please check out the documentation for more details. Next, I'd like to introduce a display doc. Display doc is a fork of this error that provides only the display-derived portion of this error, but uses doc comments instead of custom attributes to input the format strings. I find this is great for simple error types, and that often error messages
21:03
make for great documentation. Now, I'd like to move on to snafu. Snafu is another error-derived macro, much like this error. We use it the same way. We add the macro to our derive attribute, and we use some custom attributes to implement display. The main difference is that snafu also has a special helper function
21:22
called context to further reduce boilerplate when capturing context for error messages. The context function takes a result and a context selector struct, which is auto-generated by the derive macro. You may notice that the source member is missing here. This is because the context function implicitly passes along contexts
21:42
like the source and the backtrace. Making this so you only have to capture additional context that is unique to your error variant. It then internally creates the correct wrapping error variant from your enum, and you can think of this as syntax sugar for map error. Anyhow, and error also have helpers for defining new errors.
22:03
However, these functions don't actually help you define new error types. Instead, they use their own private types to implement to create the new errors, and then immediately convert those errors to their error reporting types. This is useful when you want to create an error exclusively to report them, common need when writing applications.
22:21
Though these crates do also provide some helpers for then later reacting to these ad hoc error types. Now before we continue, there are some things I'd like to share that I think are useful to know when defining errors. First, with API stability, there's an easy pitfall to run into as a library author. If you just create a public enum for your error type,
22:42
adding a new variant or even a new member to an existing variant is a breaking change and will make you implement your major version. This can be avoided simply by adding non-exhaustive to error enums where you think you may wish to add new variants in the future, or to specific variants where you think you may wish to add new members
23:01
in the future. I think it's also important to consider stack size when handling errors. With the result, the size of your return type is the larger of the two variants. If your error types are large, all functions returning your errors will require more stack space, and this can negatively impact your performance of your program, even when no errors are encountered.
23:24
The solution here is to allocate your errors on the heap when they're too large, either with a box or a reporting type like errorAnyHow, which is designed to occupy as little stack space as possible. However, when boxing your errors, remember to use the original error type, not dynError, but the box still implements the error trait.
23:43
Okay, enough of the tips. Let's move on to propagation and gathering context, starting with Feller. Feller is a library that adds support or throwing functions to us through procedural macros. Functions annotated with rows return a result where the OK type is the apparent return type
24:02
of the function, and the error type is the type listed in the throws attribute. Feller then does implicit OK wrapping for return types and provides a throws macro for returning errors. If you're missing throw syntax in Rust, this is the library for you. Next, for gathering context,
24:21
I'd like to introduce tracingError. tracingError is a tracing instrumentation library that exposes the span trace type. This library also has some cool helpers for wrapping errors with span traces and then later extracting them through dynError trait objects, working around the restrictions of the error trait. Here we use inCurrentSpan to wrap an error
24:40
with our instrument error type. Then below that, we convert that error immediately to a trait object and then extract the span trace from that error using some clever hacks. Another useful library for gathering context is extractError. extractError is an abstraction of the helpers written in tracingError for span traces.
25:01
Instead, extractError exposes a bundle type for bundling arbitrary types with errors, which can then be extracted later through dynError trait objects. Here is an example of bundled being used to bundle a backtrace-rs trace with an error instead of a std backtrace, emulating the backtrace function on the error trait in a way that works on stable today.
25:23
Next, for matching reacting, I mentioned this earlier, but Anyhow and Error also have some helpers for handling their own ad hoc created errors. Errors created with the error macro or with wrapError can be downcast back to the original type used to create the error message.
25:41
Here you can see a type that just implements display, which we use to construct a wrapping error type as the error message. Then we can later downcast through the report and you specifically have to do it through the report. You can't get a reference to the internal error, but we can downcast that report back to the message type that was used when creating that wrapping error.
26:02
Now for discarding errors, I don't know of any open source libraries that help, but if you do know of any, please let me know. For reporting though, you're already familiar with Anyhow and Error given that they are the stars of this talk. These libraries expose error reporters that as far as I can tell are strictly superior versions of boxedine error.
26:21
For customizing these reporters though, there are a couple of hook libraries I've written. ColorError you're already familiar with. StableError provides identical features to the default error hook provided in error, but using backtrace-rs backtrace instead of the standard library's backtrace, so it works on stable. JaneError is just a re-export of ColorError,
26:42
but I think it's worth mentioning just for the pun given that it's the reason I named my library error. And ColorAnyhow isn't usable yet, but the PR to add the error reporting hooks to Anyhow has already been written and looks likely to merge as soon as I've gathered enough evidence to prove the design. So please look forward to using that in the future if you're already using Anyhow.
27:02
So that's it. That's everything I wanted to tell you, but before we go, I'd like to try applying this breakdown of error handling idea to a question I see all the time in the Rust community. What library do I use when writing errors? The common answer to this question that you'll see time and again is use Anyhow if you're writing an application
27:20
and use this error for libraries. Now I agree that this is usually correct, but it's not universally true and we can cope with a better rule of them if we ask ourselves, what kinds of error handling do we need for libraries versus applications? Now for libraries, we don't generally know how our users will handle our errors.
27:41
They could report, react, wrap and propagate them, or discard them. So we need error types that are maximally flexible. This means we need to implement the error traits so they can compose with other errors and be reported. And we want our errors to be an enum so they can be reacted to easily. We also want our API to be stable so we should use non-exhaustive
28:01
so we can add new variants in the future. All of this pushes us to use error-defining libraries like derived macros or to just implement the associated traits by hand. For applications, on the other hand, we know which errors we're going to handle versus the report and we usually handle errors close to where they're returned. We will need to create new errors,
28:21
but usually this is only for errors that we wish to report. And we need to be able to report arbitrary errors because applications usually work with errors from many sources. All of this pushes us to use error-reporting libraries like error anyhow. And that's it, for real this time. That's the entire talk.
28:41
Thank you everyone for taking the time to listen and I hope you find it helpful.
Recommendations
Series of 3 media
Series of 4 media
Series of 4 media
Series of 17 media
Series of 15 media
Series of 6 media