in ,

Overview of Rust error handling libraries, Hacker News

Overview of Rust error handling libraries, Hacker News


             

Introduction

Rust’s error handling is a pleasure to use thanks to the(Result) type. It ensures Rust’s error handling is alwayscorrect,visible, andperformant. And with the addition of the? operator inRust 1. 13, and the addition of return types from main in (Rust 1.) Rust's error handling has only Keep improving.

However it seems we haven’t quite reached the end of the road yet. The Crates.io ecosystem has seen a recent addition of a fair number of error-oriented libraries that try and improve upon the status quo of Rust’s error handling.

This post is a survey of the current crates.io error library landscape .

Libraries

error-chain

error-chainis one of the earlier error handling libraries and was released inApril of 2016. A lot of what’s proposed in this library has become part of std over the years. In particular it introduced the following features:

  • The concept of“chaining” errorsto propagate error causes.
  • backtracesthat capture the stack at the point of error creation.
  • A way to create new errors through theerror_chain!macro.
  • (A)bail!macro to create new errors. (An)Ensuremacro to create error-based assertions. (A)quick_main!macro to allow returning (Result) frommainwith anExitCode.

// Fallible main function.quick_main! (|| ->Result{// Chain errors to provide context.letres: Result=do_something().chain_err(|| "something went wrong");// Error-based assertions.ensure! ((2)>1), "num too big");// Return ad-hoc errors from strings.bail! ("error")});

Looking at this library it’s quite obvious it’s aged a bit. But in a good way; because many of its innovations have become part of everyday error handling in Rust, and the need for the library has lessened.

(failure)

Failureis the spiritual successor toerror-chain, and was released in (November of) **********************************************************************************************************************************************************************************. It comes with an analysis of the limitations of the error trait, and introduced a new trait,(Fail), that served as a prototype of how to overcome those limitations. In particular it came with the following features:

//! A basic example of failure at work.usefailure :: {ensure, Error, ResultExt};(FN)check((num):(usize)) ->Result{ensure! (num12, "number exceeded threshold");Ok (())}(FN)main() ->Result{check(6).(context)("Checking number failed.") ?;check(13).(context)("Checking number failed.") ?;Ok (())}

It seems asfailurewas designed in anticipation ofmainbeing able to returnResult, and so unlikeerror-chainit doesn’t bother with that. Also it brings back the ability to define an [ErrorErrorKind]. It feels like the library was equal parts trying to improve defining new errors for use in libraries, as ad-hoc errors for use in applications.

context-attribute

context-attributewas a library that I wrote in May of 2019 to solve the locality problem offailure‘sResultExt :: contextmethod.

Namely when you’re applying.Contextonto a function, what you’re really doing is describing what that function does. This is so that when an error occurs from that function, we’re provided with human-readablecontextof what we were trying to do. But instead of that context being provided by the function we’re calling, the context needs to be set by the (caller) ****************************************.

Not only does it feel like the context and the error site are detached when using. context (), it makes the calling code harder to read because of all the inline documentation strings.

//! Error context being provided at call site.(FN)square((num):(usize)) ->Result (usize), failure :: Error>{ensure! (num10, "Number was too large");Ok (num * num)}(FN)main() ->Result{square(2)*(2)).context("Squaring a number failed") ?;square(10*10).context("Squaring a number failed") ?;Ok (())}
// Error context being provided during definition./// Square a number if it's less than 10.# [context](FN)square((num):(usize)) ->Result (usize), failure :: Error>{ensure! (num10, "Number was too large");Ok (num * num)}(FN)main() ->Result{square(2)*(2)) ?;square(10*10) ?;Ok (())}

(err-derive)

err-derive (is a) failure- like derive macro forstd :: error :: Errorfirst released in December of 2018. It’s motivation for existing is that sincestd :: error :: Erroris going to be gaining many of the benefits offailure :: Fail, there should be a macro to use just that.

Features it provides are:

//! Error-derive in action.usestd :: error :: Error;# [derive(Debug, derive_error::Error)]pub enumLoadingError {# [error(display="could not decode file")]FormatError (# [error(cause)] FormatError),# [error(display="could not find file: {:?}", path)]NotFound {path: PathBuf},}(FN)print_error((e): & dyn Error) {eprintln! ("error: {}", e);let mutcause=e.source();while letSome (e)=cause {eprintln! ("caused by: {}", e);cause=e.source();}}

auto_enums

auto-enumsis a proc macro prototype of a language feature to enable more flexibleimpl Traitreturn types. But sinceErroris a trait, it’s usesextend to error handling too. It was first released in December of 2018.

auto-enumsunfortunately doesn’t allow anonymousimpl Erroryet insideResult, but itcancreate an auto-derive forErrorif all variants’ inner values ​​in an enum implementError.

//! Quickly create a new error type that's the sum of several other error types.useauto_enums :: enum_derive;usestd :: io;# [enum_derive(Error)](ENUM)Error {Io (std :: io :: Error),Fmt (std :: fmt :: Error),}(FN)foo() ->Result{leterr=io :: Error :: new (io :: ErrorKind :: Other, "oh no");Err (Error :: Io (err))}(FN)main() ->Result{foo() ?;println! ("Hello, world!");Ok (())}

But presumably ifImpl Traitinauto_enumswould work forErrorthe way it does for other traits, the error enum would be anonymous and created on the fly. Which would allow us to do multiple operations with different error types in a single function without declaring a new error type.

usestd :: error :: Error;usestd :: fs;(FN)main() ->Result{letnum=(i8):: from_str_radix ("(A)" ,16) ?;letfile=fs :: read_to_string ("./ README.md" ) ?;Ok (())}

snafu

snafuis an error handling library first released in January 2019. The purpose of it is: “to easily assign underlying errors into domain-specific errors while adding context “.

It seems to draw from experiences of usingfailure, andships a comparisonas part of the provided documentation. A primary difference seem to be thatfailuredefines a new trait (Fail) , whilesnafuusesstd :: error :: Error. Snafu also don’t thinks having shorthands to create errors from strings is very important, stating:“It’s unclear what benefit Failure provides

Snafu provides the following features:

// A small taste of defining & using errors with snafu.# [derive(Debug, Snafu)](ENUM)Error {# [snafu(display("Could not open config from {}: {}", filename.display(), source))]OpenConfig {filename: PathBuf,source: std :: io :: Error,},# [snafu(display("The user id {} is invalid", user_id))]UserIdInvalid {user_id:(i), backtrace: Backtrace},}(FN)log_in_user(filename(*************************************************************: Path, (ID):usize) ->Resultbool, std :: error :: Error>{letconfig=fs :: read (filename).(context)(Error: : OpenConfig {filename}) ?;ensure! (id==42, Error :: UserIdInvalid {user_id});(Ok)(true))}

Fehler

Fehleris a new error library by the inimitable Boats, released in September of 2019 (the same author asfailure)). It feels like an experimental attempt to answer the question of: “What if we introduced checked exception syntactic sugar for Rust’s error handling “. It seems to propose that whatasync / .awaitis to (Future) , (throw / throws) would be toResult.

This library was only released after theBacktracefeaturelanded on nightly, andError :: sourcelanded inRust 1..

The innovation it provides here is the removal of the need toOkwrap each value at the end of a statement. MeaningOk (())is no longer needed. Addition creating new errors can be done using theerror!macro. And returning errors from the function (“throwing”) errors can done using theThrow! (macro.)

Each function is annotated using thefehler :: throwsattribute. It optionally takes an argument for the error type, such asio: : Error. If the error type is omitted, it assumesBox.

edit: the following section is slightly out of date as fehler merged a radical simplification to their API4 hours ago. It now only exposesthrowand (throws) , and allow specifying your own default error type.

Unlikefailure, this library only defines methods for propagating errors and creating string errors. It doesn't have any of the extra error-definition utilitiesfailurehad. In particular it provides:

  • Creating new errors from Strings througherror!.
  • Returning errors from functions usingThrow!.
  • Removing the need to writeResult in the function signature, Ok-wrapping, and dynamic errors throughthrows!.
  • A way to propagate error causes throughContext.
  • Printing a list of error causes when returned frommain.
//! Error handling relying only on stdlibusestd :: error :: Error;AsyncFNsquare(x):(i)) ->Resulti 32, Box'static>>{ifx>12{Err (format!) ""Number is too large")) ?;}Ok (x * x)}
//! An example of Fehler in use.usefehler :: {throws, throw, error};# [throws]AsyncFNsquare(x):(i)) ->i 32{ifx>12{throw! (error! ("Number is too large"));}x * x}
//! If Fehler's proc macros were directly implemented as lang features.AsyncFNsquare(x):(i)) ->i 32throws {ifx>12{throw "Number is too large";}x * x}

Anyhow

Anyhowis an error-handling library for applications, and was first released on October 7th, 2019. It was built to make it easier to propagate errors in applications, much like Fehler. But instead of prototyping new language features, it defines a few new types and traits; with zero dependencies. Its tag line is: "A betterBox".

This library was only possible after the implementation oflanded, and it requires Rust 1. 34 in order to function. The main features it provides are:

  • Dynamic errors throughError.
  • Conveniently iterating overcausesthroughChain.
  • Creating new errors from strings throughAnyhow!/format_err!.
  • Returning from functions early with new errors throughbail!
  • Error-based assertions throughEnsure!.
  • The ability to extendResult withcontextthrough(Context).
  • A shorthand type(Result)forResult.
  • Printing a list of error causes when returned frommain.
//! An example using anyhow.useanyhow :: {Context, Result};(FN)main() ->Result{...it.Detach().context("Failed to detach the important thing") ?;letcontent=std :: fs :: read (path).with_context(|| format! ("(Failed to read instrs from){}", path)) ?;...}

thiserror

thiserrorwas written by the same author,dtolnay, and released in the same week asAnyhow. Where bothfehlerand (anyhow) cover dynamic errors,thiserroris focused on creating structured errors.

In a way,Anyhowandthiserrorfeel like they have taken the wide ranger of featuresfailureintroduced in 2017, and split the domain between them.thiserroronly contains a single trait to define new errors through:

usethiserror :: Error;# [derive(Error, Debug)]pub enumDataStoreError {# [error("data store disconnected")]Disconnect (# [from] io :: Error),# [error("the data for key `{0}` is not available")]Redaction (String),# [error("invalid header (expected {expected:?}, found {found:?})")]InvalidHeader {expected: String,found: String,},# [error("unknown data store error")]Unknown,}

thiserror's derive formatting was fairly unique at the time of release, and has since been extracted into thedisplaydoccrate to implement# [derive(Display)] (for structs and enums.)

std :: error :: Error

And last but not least,std's Error traithas come a long way since the early days. A notable few changes have happened over the past few years:

  • Error :: descriptionwas soft-deprecated in favor of (impl Display) .
  • Error :: causehas been deprecated in favor ofError :: source.
  • Error :: iter_chainandError :: iter_sourcesare now available on nightly.
  • Error :: backtraceand theBacktracemodule are now available on nightly.

In addition withResultfrom main, exit codes, and the?operator we've definitely come a long way from the early days of Rust's error handling.

Features

So far we've taken a look at 9 different error handling libraries , and you might have noticed that they have a lot in common. In this section we'll try and break down which features libraries introduce, and what their differences are.

Returning Result from main

error-chainintroduces the ability to returnResultfrom main, and this was added to Rust in1. 26.

Error-based assertions

In the stdlib theAssert!andassert_eq!macros exist. These are convenient shorthands to check a condition, and panic if it doesn't hold. However, it's quite common to check a condition, and want to return an error if it doesn't hold. To that extenterror-chain,failure, (snafu) , andAnyhowintroduce a new macro:ensure!.

The largest difference between the macros is which values ​​are valid in the right-hand side of the macro. Insnafuonly structured errorsare valid, infailureonly strings are valid, and inAnyhowBoth are valid.

//! Strings in the right-hand side to create new, anonymous errors.ensure! (user==0, "only user 0 is allowed" );
//! Structured errors in the right-hand side.usestd :: io :: {Error, ErrorKind}ensure! (depthMAX_DEPTH, Error :: new (ErrorKind :: Other, "(oh no)"));

(Causes)

Many libraries implement the ability to recursively iterate overError :: source(formerlyError :: cause). These libraries includeerror-chain,failure,Fehler, andAnyhow.

In addition,Fehlerandanyhowwill print the list of causes when returned fromfn main.

Error: Failed to read instrs from ./path/to/instrs.jsonCaused by:No such file or directory (os error 2)

err-derivedoesn't provide any facilities for this, but recommendsusing a pattern to print errors. This could be used with other libraries such assnafuas well. But it misses the "print from main" functionality offehlerandAnyhow. And it doesn't provide the same convenienceanyhow :: Chaindoes either by implementingDoubleEndedIteratorand (ExactSizeIterator) .

forcause in error.chain() {println! ("error:{}", cause )}

Backtraces

error-chain,failure, andSnafuadd backtrace support to their error types. Backtrace support is currently available on nightly, andfehler, andAnyhowmake use of this. It seems that if this feature becomes available on stable, backtrace support for errors will mostly have been covered.

Creating errors from strings

failure, andanyhowprovide aformat_err!macro that allows constructing a boxed error from a string.Anyhowhas a type alias called (anyhow) with the same functionality.

Fehlerprovides a macroerror!to similarly construct new errors from strings.

letnum=21;return(Err) format_err! ("{} kitten paws", num));

snafudoes not provide a dedicated macro for creating errors from strings, but instead recommends using (a pattern) .

(FN)   (example)() ->Result>{Err (format!) ""Something went bad:{}",(1)1))) ?;Ok (())}

Early returns with string errors

error-chain,failure, andAnyhowProvide abail!macro that allows returning from a function early with an error constructed from a string.

Fehlerhas a model whereErrand (Ok) no longer are required, and provides athrow!macro to return errors. Anderror!macro to create new errors from strings.

(if)!has_permission(user, resource) {bail! ("permission denied for accessing {}", resource);}

snafudoes not provide a dedicated macro for creating errors from strings, but instead recommends using theErr (err)?Pattern.

Context on Result

failure,snafu,fehler, andAnyhowallow extendingResult withcontextmethod that wraps an error in a new error that provides a new entry forsource, effectively providing an extra layer of descriptions.

It's unclear how each library goes about this, but especiallyAnyhowseems to Havegone to great Lengthsto prevent it from interacting negatively with downcasting.

(FN)do_it() ->Result{helper().context("Failed to complete the work") ?;// ...}

error-contextprovides a way to move.Contextfrom the call site to the error definition through doc comments.

/// Complete the work# [context](FN)Helper() ->Result{// ...}(FN)do_it() ->Result{helper() ?;}

(Custom errors)

failure,err_derive,auto_enums,snafu, andthiserrorProvide derives to construct new errors. They each go about it slightly differently.

//! Failure# [derive(failure::Fail, Debug)]# [fail(display="Error code: {}", code)]structRecordError {code:U 32,}
//! err_derive/// `MyError :: source` will return a reference to the` io_error` field# [derive(Debug, err_derive::Error)]# [error(display="An error occurred.")]structMyError {# [error(cause)]io_error: io :: Error,}
//! snafu# [derive(Debug, Snafu)](ENUM)Error {# [snafu(display("Could not open config from {}: {}", filename.display(), source))]OpenConfig {filename: PathBuf,source: std :: io :: Error,},# [snafu(display("The user id {} is invalid", user_id))]UserIdInvalid {user_id:(i), backtrace: Backtrace},}
//! thiserror# [derive(thiserror::Error, Debug)]pub enumDataStoreError {# [error("invalid header (expected {expected:?}, found {found:?})")]InvalidHeader {expected: String,found: String,},# [error("the data for key `{0}` is not available")]Redaction (String),}
# [enum_derive(Error)](ENUM)   (Enum)  {A (A),B (B),}

failure,err_derive, andSnafuall seem to make use of format-string like interpolation inside inner attributes on structs to define custom error messages.thiserroris different, and provides a more inline style of formatting.

auto_enumsis yet again different, in that it doesn't provide the ability to define new error messages, but will delegate to the error implementation of its member's inner values.

(Other / Miscellaneous)

This is a list of miscellaneous developments around errors that seemed somewhat related, and generally interesting.

(Try blocks)

try {}blocks are anunstable featurethat introduces a new kind of block statement that can "catch" error propagation using the?operator, and doesn't require (Ok) wrapping at the end of the block.

This is useful for when for example wanting to handle multiple errors in a uniform way.

Async(FN)read_file( (file): Path) ->io :: Result{letfile=try {path.is_file(). await ?;letfile=fs :: read_to_string (file) .await ?;file}.context("error reading the file") ?;println! ("first 5 chars{:?}", file [0..5]);Ok (())}

This feature seems to have some overlap withfehler'sThrow/throwssyntax; and the interaction between the two language directions is definitely

Verbose IO errors

When reading a file from a path fails, you'll often see an error like this:

Finished dev [unoptimized   debuginfo] target (s) in 0. 56 sRunning `target / debug / playground`Error: Os {code: 2, kind: NotFound, message: "No such file or directory"}

This is often quite unhelpful.Whichfile did we fail to read? What should we do to make it pass? We can usually find out with a bit of debugging. But if the error comes from a dependency, or a dependency's dependency, figuring it out can often become a huge chore.

Inasync-stdwe're in the process ofadding "verbose errors "that contain this information, and should reduce compilation times. Because it's off the hot path, and we already allocate regularly, it should hardly have any impact on performance.

Returning verbose errors by default in the stdlib may not be the right fit . But this is definitely something that has come up in the past, and will continue to come up in the future unless it's addressed. But as always, the hardest question to answer ishow.

Error pointer registers

Apparently Swift has really efficient error handling usingtry / catch. It Reservesa dedicated registerfor error pointers, zeroes it, then checks if it's still 0. The way it achieves this is through a dedicatedswifterrorinstruction in LLVM.

It's been mentioned that this is something that could be done for Rust's?as well, though nobody has the resources to.

Analysis

It seems likeerror-chain, andfailuremarked some big upsets in error handling for Rust. And since their release some of the value they've provided has been merged back into the language. Sinceerror-chain(Result) fromfn mainhas become possible. And sincefailure,Error :: causeshas been deprecated in favor ofError :: source. And backtrace support has become available on nightly.

Utilities

However, throughout the years some features have been consistent among crates, but never found their way into the standard library. In particularformat_err!,Ensure!, andbail !are recurring in a lot of libraries, but haven't made their way in. Having more readily available methods of creating and using string-based errors seems to be a desirable feature.

Creating errors

Another common theme is the availability of acontextmethod onResult. There seems to be almost universal consensus among library authors that this is desirable, yet it has no counterpart in stdlib yet.

From there it seems libraries can roughly be divided into two categories: dynamic errors and structured errors. This split is best evidenced in the sibling libraries ofanyhow, andthiserror.

In terms of dynamic error handlingfailure'sFailtype started the current trend, andfehlerand (anyhow) seem to have modernized it with what's been made available in stdlib since. In particular these libraries provide a better version ofBox.

In terms of defining new errors,Snafuandthiserrorseem to have made the most progress sincefailure. They mainly differ in the way serialization of new errors works.

Language developments

In terms of language experimentations there have also been exciting developments.auto_enumshypothesises that if (impl Trait) would work, anonymous enums could be created for types that can delegate to their inner implementations. Andfehlerproposes syntactic sugar to remove (Result) from error use entirely, instead using checked exceptions through thethrowandthrowsKeywords.

The similarities betweenThrow,throws, andtrycould plausibly help lower the barrier for learning Rust as they have much in common with mainstream programming languages ​​such as JavaScript, Swift, and Java. But ideally further research would be conducted to help corroborate this.

Conclusion

In this post we've looked at 9 different error handling libraries, the features they propose, and laid out how they compare to other libraries in order to determine recurring themes.

There seems to be rough consensus in the ecosystem that we seem to need :

  • Some kind of replacement for (Box)

  • Some way of wrapping (Results) in.Context.
  • Some way to conveniently define new error types.
  • Some way to iterate over error causes (# 58520).
  • Support for backtraces (# 53487).
  • Additionally the functionality provided byensure!,bail!, andformat_err!has been exported as part of many of the libraries, and seems to be along the lines of: "stable, popular, and small" that isthe bread and butter of STD.

    Since starting work on this post, two others ( (1) , (2) ) have written about error handling. Which seems indicative error handling is something that's on people's mind right now.

    This is all I have to share on error handling at the moment. I hope this post will be prove to be useful as people set out to improve Rust's error handling.

    Thanks to: Aleksey Kladov, Niko Matsakis, and Alex Crichton for reading reviewing this post and helping refine it.

        

      

    Brave Browser
    Read More
    Payeer

    What do you think?

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    GIPHY App Key not set. Please check settings

    Backend Engineer | Standard Cyborg, Hacker News

    Backend Engineer | Standard Cyborg, Hacker News

    Hot takes as opinion cools on Tesla Cybertruck, Ars Technica

    Hot takes as opinion cools on Tesla Cybertruck, Ars Technica