Error handling Isn't All About Errors
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.
Lusby, Jane
Error handling Isn't All About Errors by Jane Lusby Error handling in rust is incredibly diverse and constantly evolving. As such it is often a source of confusion for new and experienced rustaceans alike. This talk aims to clarify what error handling in rust means, the patterns that exist, when to apply them, and what libraries currently exist to help with the task.
 Hello, and welcome to my talk. Air handling isn't all about errors. Let me start by introducing myself. My name is Jane Lesby and my pronouns are she, though. On the internet, I go by yaw or yaw-see. I've been writing rust for about two and a half 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. 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, there are more than just these two mentors, so please check out the website. Moving on. So, why air handling? I actually got into air handling on accident. It started as a yak shave when I wanted to open source a library that I wrote for work, but I wasn't happy with the air handling, so I decided to fix it up first. That yak shave ended with me writing air. Air is a fork of anyhow with support for customized air reports via a global hook, similar to a panic hook. I also ended up writing color air. Color air is a library which provides a custom panic report hook and a custom air report hook for air. With these libraries, I'm now able to construct air reports that look like this. Here we can see the basic usage example. In it, we have an air 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. 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 five frames before read file and 10 frames after main. We can also take this further though by applying custom filters. 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 main frame. We can apply this custom filtering consistently to all of our air reports. So here you can see we have the same report format for panics as we did for our errors. And with our air 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 stood error section, showing the output of the command when it failed. We'll take 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 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 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 in their gathered context. Now, this breakdown gets to the goal of my talk. 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 worried 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 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 Rust books chapter on error handling. The Rust 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. 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 indexed out of bounds or integer overflow. 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 recoverable errors and result for non-recoverable errors. Panic for non-recoverable errors and result for recoverable errors. Sorry. So panic. Non-recoverable errors in Rust are created via the panic macro. Here we can see an example of an indexed 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. And by default context gathering, I mean capturing the caller location or capturing the back trace 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. 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. 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. 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. 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 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. 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 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. 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. So, we define recoverable errors with types and traits, propagate them with the quick try operator. For matching and reacting, we do that with match or downcasts depending on how it's represented. We discard them with drop or unwrap if we want to promote a recoverable error to an unrecoverable one and for reporting we use the error trait. For nonrecoverable 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 nonrecoverable errors. Like, you don't do this one thing for nonrecoverable 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 language and where they 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, whereas context is any information relevant to an error or an error report that is not itself an error. And an airport is the printed representation of an error in all of its associated contexts. Now, the concept of error reports and error porters isn't a concept that is common in the Rust ecosystem today, or any languages error handling ecosystem as far as I know. 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 in 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 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 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 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. 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. The error trait equivalent in other languages is often quite simple. This is 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 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 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. 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, so 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 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 and error was constructed, HTTP status codes, custom report sections, and more. If we could access these generic forms of context in error port, we could implement 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 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 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 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 accident successfully. 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 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. So let's see how this changes things. Error, command accident successfully, command get cat. Okay, so we can see why the command failed. Cat isn't a real get 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 the more descriptive, or let's just go ahead and define a new error with the 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 hopes. And then we create a command to run get 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 for which is 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 of the original one as an error report. So let's go ahead and run this. And we can see error. The cat cannot be got because the command exited unsuccessfully and the command was get cat. Cool. This is pretty helpful. It's not quite there yet. We're still missing the stood error output that the original example had. So let's go ahead and add that finally. So we're going to convert stood error to a string just like we did with stood out. And here we're going to show both stood out and stood error as sections into the final error port. Let's go ahead and see what we get. Error cat cannot get got and exited unsuccessfully. Man is get cat and cat isn't a get command. Here's the suggestions for what we could run instead. Perfect. We finally have an error port 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 ports. And help you understand why I think error porting 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. 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 porters that I think are useful to know. So first of all, error porters almost always implement from E for all error types. Now, this is because error porters exist to generically format any error and they're 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 porters cannot implement the error trait themselves. This is true for all types that implement from E for all errors, including anyhow error, error port and box diner. Implementing both of these traits ends up violating the overlap rule in that there would be two from impulse to choose from when converting a reporter to itself. So thanks to the fact that they can implement the error trait, they also don't compose very well with other errors. 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 language, how they fit into the various pieces of error handling, and have an understanding of how they can be combined to write error reports. So 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 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 derive attribute. Next, we can implement display with this error via error attributes. We can have a simple attribute, which contains only a string literal. 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. 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 front attribute, and note that this also implies the source attribute when used. This isn't all this error can do, so please check out the documentation for more details. Next, I'd like to introduce the display doc. Display doc is a fork of this error that provides only the display derived portion of this error, but uses.comments instead of custom attributes to input the format strings. I find this is great for simple error types, and that often error messages 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 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 derived macro. You may notice that the source member is missing here. This is because the context function implicitly passes along contexts 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 for your enum, and you can think of this as syntax sugar for map error. Anyhow, an error also has helpers for defining new errors. 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, commonly when writing applications, 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, 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 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. The solution here is to allocate your errors on the heap when they're too large, either with a box or reporting type, like error anyhow, which is designed to occupy as little stack space as possible. However, when boxing your errors, remember to use the original error type, not dine error, but the box still implements the error trait. Okay, enough of the tips. Let's move on to propagation and gathering context. Starting with Feller. Feller is a library that adds support for throwing functions to us through procedural macros. Functions annotated with rows, return a result where the okay type is the apparent return type of the function, and the error type is the type listed in the throws attribute. Feller then does implicit okay wrapping for return types and provides a throws macro for returning errors. If you're missing throws syntax and rust, this is the library for you. Next, for gathering context, I'd like to introduce tracing error. Tracing error 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 dine error trait objects, working around the restrictions of the error trait. Here we use in current span to wrap an error with our instrumented 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 extract error. Extract error is an abstraction of the helper's written tracing error for span traces. Instead, extract error exposes a bundle type for bundling arbitrary types of errors, which can then be extracted later through dine error trait objects. Here is an example of bundled being used to bundle a backtrace-rs backtrace with an error instead of a std backtrace emulating the backtrace function on the error trait in a way that works on stable today. 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 wrap error meet downcasts back to the original type used to create the error message. 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. 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 they are the stars of this talk. These libraries expose error reporters that, as far as I can tell, are strictly superior versions of Box Dyn Error. For customizing these reporters, though, there are a couple of hook libraries I've written. Color Error you're already familiar with. Stable Error provides identical features to the default error hook provided in error, but using backtrace-rs backtrace instead of the standard libraries backtrace, so it works on stable. Jane Error is just a re-export of Color Error, but I think it's worth mentioning just for the pun, given that it's the reason I named my library Error. Color Anyhow 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. 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 to use Anyhow if you're writing an application and use this error for libraries. Now, I agree that this is usually correct, but it's not universally true. And we can call it a better rule of thumb 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. They could report, react, rapid 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 in Enum so they can be reacted to easily. We also want our API to be stable, so we should use non-exhaustive so we can add new variants in the future. All of this pushes us to use error-defining libraries like derived macros where to just implement the associated traits by hand. For applications, on the other hand, we know which errors we're going to handle versus a report, and we usually handle errors close to where they're returned. We will need to create new errors, 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 and anyhow. And that's it for real this time. That's the entire talk. Thank you everyone for taking the time to listen, and I hope you find it helpful. Thank you.
 Hello, and welcome to my talk.  Air handling isn't all about errors.  Let me start by introducing myself.  My name is Jane Lesby and my pronouns are she, though.  On the internet, I go by yaw or yaw-see.  I've been writing rust for about two and a half 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.  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, there are more than just these two mentors,  so please check out the website.  Moving on.  So, why air handling?  I actually got into air handling on accident.  It started as a yak shave when I wanted to open source a library  that I wrote for work, but I wasn't happy with the air handling,  so I decided to fix it up first.  That yak shave ended with me writing air.  Air is a fork of anyhow with support for customized air reports  via a global hook, similar to a panic hook.  I also ended up writing color air.  Color air is a library which provides a custom panic report hook  and a custom air report hook for air.  With these libraries, I'm now able to construct air reports  that look like this.  Here we can see the basic usage example.  In it, we have an air 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.  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 five frames before read file  and 10 frames after main.  We can also take this further though by applying custom filters.  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 main frame.  We can apply this custom filtering consistently  to all of our air reports.  So here you can see we have the same report format  for panics as we did for our errors.  And with our air 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 stood error section,  showing the output of the command when it failed.  We'll take 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 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 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 in their gathered context.  Now, this breakdown gets to the goal of my talk.  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 worried 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 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 Rust books chapter on error handling.  The Rust 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.  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 indexed out of  bounds or integer overflow.  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 recoverable errors and result for non-recoverable errors.  Panic for non-recoverable errors and result for recoverable errors.  Sorry.  So panic.  Non-recoverable errors in Rust are created via the panic macro.  Here we can see an example of an indexed 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.  And by default context gathering, I mean capturing the caller location  or capturing the back trace 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.  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.  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.  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.  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 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.  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 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.  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.  So, we define recoverable errors with types and traits, propagate them with the quick try operator.  For matching and reacting, we do that with match or downcasts depending on how it's represented.  We discard them with drop or unwrap if we want to promote a recoverable error to an unrecoverable one  and for reporting we use the error trait.  For nonrecoverable 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 nonrecoverable errors.  Like, you don't do this one thing for nonrecoverable 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 language and where they 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,  whereas context is any information relevant to an error or an error report that is not itself an error.  And an airport is the printed representation of an error in all of its associated contexts.  Now, the concept of error reports and error porters isn't a concept that is common in the Rust ecosystem today,  or any languages error handling ecosystem as far as I know.  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 in 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 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 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 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.  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.  The error trait equivalent in other languages is often quite simple.  This is 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 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 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.  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,  so 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 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 and error was constructed,  HTTP status codes, custom report sections, and more.  If we could access these generic forms of context in error port, we could implement 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 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 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 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 accident successfully.  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 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.  So let's see how this changes things.  Error, command accident successfully, command get cat.  Okay, so we can see why the command failed.  Cat isn't a real get 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 the more descriptive,  or let's just go ahead and define a new error with the 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 hopes.  And then we create a command to run get 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 for which is 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 of the original one as an error report.  So let's go ahead and run this. And we can see error.  The cat cannot be got because the command exited unsuccessfully and the command was get cat.  Cool. This is pretty helpful. It's not quite there yet.  We're still missing the stood error output that the original example had.  So let's go ahead and add that finally.  So we're going to convert stood error to a string just like we did with stood out.  And here we're going to show both stood out and stood error as sections into the final error port.  Let's go ahead and see what we get. Error cat cannot get got and exited unsuccessfully.  Man is get cat and cat isn't a get command.  Here's the suggestions for what we could run instead. Perfect.  We finally have an error port 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 ports.  And help you understand why I think error porting 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.  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 porters that I think are useful to know.  So first of all, error porters almost always implement from E for all error types.  Now, this is because error porters exist to generically format any error and they're 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 porters cannot implement the error trait themselves.  This is true for all types that implement from E for all errors, including anyhow error, error port and box diner.  Implementing both of these traits ends up violating the overlap rule in that there would be two from impulse to choose from when converting a reporter to itself.  So thanks to the fact that they can implement the error trait, they also don't compose very well with other errors.  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 language, how they fit into the various pieces of error handling,  and have an understanding of how they can be combined to write error reports.  So 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 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 derive attribute.  Next, we can implement display with this error via error attributes.  We can have a simple attribute, which contains only a string literal.  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.  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 front attribute, and note that this also implies the source attribute when used.  This isn't all this error can do, so please check out the documentation for more details.  Next, I'd like to introduce the display doc.  Display doc is a fork of this error that provides only the display derived portion of this error, but uses.comments instead of custom attributes to input the format strings.  I find this is great for simple error types, and that often error messages 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 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 derived macro.  You may notice that the source member is missing here.  This is because the context function implicitly passes along contexts 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 for your enum, and you can think of this as syntax sugar for map error.  Anyhow, an error also has helpers for defining new errors.  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, commonly when writing applications, 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, 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 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.  The solution here is to allocate your errors on the heap when they're too large, either with a box or reporting type, like error anyhow, which is designed to occupy as little stack space as possible.  However, when boxing your errors, remember to use the original error type, not dine error, but the box still implements the error trait.  Okay, enough of the tips. Let's move on to propagation and gathering context. Starting with Feller.  Feller is a library that adds support for throwing functions to us through procedural macros.  Functions annotated with rows, return a result where the okay type is the apparent return type of the function, and the error type is the type listed in the throws attribute.  Feller then does implicit okay wrapping for return types and provides a throws macro for returning errors.  If you're missing throws syntax and rust, this is the library for you.  Next, for gathering context, I'd like to introduce tracing error.  Tracing error 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 dine error trait objects, working around the restrictions of the error trait.  Here we use in current span to wrap an error with our instrumented 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 extract error.  Extract error is an abstraction of the helper's written tracing error for span traces.  Instead, extract error exposes a bundle type for bundling arbitrary types of errors, which can then be extracted later through dine error trait objects.  Here is an example of bundled being used to bundle a backtrace-rs backtrace with an error instead of a std backtrace emulating the backtrace function on the error trait  in a way that works on stable today.  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 wrap error meet downcasts back to the original type used to create the error message.  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.  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 they are the stars of this talk.  These libraries expose error reporters that, as far as I can tell, are strictly superior versions of Box Dyn Error.  For customizing these reporters, though, there are a couple of hook libraries I've written.  Color Error you're already familiar with.  Stable Error provides identical features to the default error hook provided in error, but using backtrace-rs backtrace instead of the standard libraries backtrace, so it works on stable.  Jane Error is just a re-export of Color Error, but I think it's worth mentioning just for the pun, given that it's the reason I named my library Error.  Color Anyhow 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.  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 to use Anyhow if you're writing an application and use this error for libraries.  Now, I agree that this is usually correct, but it's not universally true.  And we can call it a better rule of thumb 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.  They could report, react, rapid 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 in Enum so they can be reacted to easily.  We also want our API to be stable, so we should use non-exhaustive so we can add new variants in the future.  All of this pushes us to use error-defining libraries like derived macros where to just implement the associated traits by hand.  For applications, on the other hand, we know which errors we're going to handle versus a report, and we usually handle errors close to where they're returned.  We will need to create new errors, 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 and anyhow.  And that's it for real this time. That's the entire talk.  Thank you everyone for taking the time to listen, and I hope you find it helpful.  Thank you.
RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_0_107.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_108_203.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_204_275.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_276_407.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_408_551.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_552_1031.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_1032_1523.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_1524_1955.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_1956_2459.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_2460_2579.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_2580_3539.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_3540_3863.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_3864_5207.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_5208_5375.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_5376_6779.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_6780_8075.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_8076_10811.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_10812_11255.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_11256_11615.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_11616_11735.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_11736_12635.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_12636_14867.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_14868_14951.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_14952_15911.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_15912_16379.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_16380_17195.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_17196_18083.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_18084_19739.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_19740_20675.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_20676_22763.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_22764_24227.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_24228_25151.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_25152_25571.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_25572_25871.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_25872_27215.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_27216_29279.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_29280_29615.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_29616_31667.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_31668_32987.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_32988_33683.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_33684_35687.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_35688_36539.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_36540_38135.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_38136_39251.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_39252_39599.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_39600_40523.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_40524_42191.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_42192_42935.png RustConf_2020_-_Error_handling_Isn_t_All_About_Err_rAF8mLI0naQ_1_1_42936_43186.png