Error handling in Rust
blog.burntsushi.net
blog.burntsushi.net
I wrote an RSS reader in Rust, and all the code required to compose all the error types (I/O, HTTP, XML, RSS, date formats) into a custom error type is as bulky as the modest amount of code that does the useful work. That's not a good thing. Having to write all that error handling code up front is going to be a big problem for the Agile crowd. This may be a major drag on Rust adoption.
All this error machinery is in Rust to avoid exceptions. That may have been a mistake. Exceptions have the good property that they can't be ignored - if they're not handled, you get a reasonable program termination. If there's a reasonable exception hierarchy, you can catch an exception near the root of the tree and get all subsidiary exceptions. (It took three major revisions of exception handling before Python got that right, but it finally did.) Rust doesn't have an error hierarchy like that; you can't just check for "RuntimeError" or "EnvironmentError" and get all the more detailed errors. You have to combine error types yourself, as the article shows.
I really want Rust to succeed, but I think they took a wrong turn with error handling.
Now, checked exceptions in Java were not fun back in the day :)
This would probably be the main point where I'd disagree. To get the same guarantees as you currently have, the exceptions would have to be checked exceptions like in Java. And, in contrast, the advantage (to me) here is that all error handling in Rust basically works like handling anything else.
Personally, I'm rather fond of the way Rust does it. Which surprised me, coming from a dynamic background.
Checked exceptions make you check for errors almost everywhere. Rust makes you check everywhere. They're almost the same thing. But I'll take Rust's method over checked exceptions any day; all is better than almost all. And for the type of projects Rust is built for, it's the right choice. But for error handling in general, if you don't care about the cost, unchecked exceptions are superior to both.
I thought paragraph 1 boiled down to: "you shouldn't see any error handling code most places; only at the top."
Then in the 2nd paragraph, I interpret "all is better than almost all" as: "I want to be forced to check all exceptions all the time, not just a few. (Meaning, I want error handling code everywhere)."
Again, not being argumentative. Just asking for clarification.
Exceptions have some advantages and disadvantages; the advantages are what I said above about only needing to write small amount of code. The disadvantages are that it does have some compiled code-size, performance, and language runtime requirements.
The nature of Rust sort of precludes it's use of exceptions and instead they have a reasonable error return facility that is a pretty type-safe way of forcing you to handle or propagate errors. Smaller compiled code, not much performance overhead, and it's easy to reason about because it's consistent. Of course, it's also a lot more boilerplate code to write.
Checked exceptions, on the other hand, negate the advantages of exceptions and still have all the other disadvantages of exceptions. It's also a lot harder to reason about than Rust's model of just handling all errors manually.
Furthermore, exceptions have this nasty codegen tradeoff. Either you make them zero-cost (as C++, Obj-C, and Swift compilers typically do), in which case throwing an exception is very expensive at runtime, or you make them non-zero-cost (as Java HotSpot and Go 6g/8g do), in which case you eat a performance penalty for every single try block (in Go, defer) even if no exception is thrown. For a language with RAII, every single stack object with a destructor forms an implicit try block, so this is impractical in practice.
The performance overhead of zero-cost exceptions is not a theoretical issue. I remember stories of Eclipse taking 30 seconds to start up when compiled with GCJ (which used zero-cost exceptions) because it throws thousands of exceptions while starting.
The C approach to error handling has a great performance and code size story relative to exceptions when you consider both the error and success paths, which is why systems code overwhelmingly prefers it. It has poor ergonomics and safety, however, which Rust addresses with Result. Rust's approach forms a hybrid that's designed to achieve the performance of C error handling while eliminating its gotchas.
Then maybe the solution is to not have it throw thousands of exceptions while starting?
And have them be, well, exceptional?
I can understand something like an unmounted drive or a network being down leading to a slow Eclipse launch due to thousands of exceptions.
But those 2 are exceptional circumstances in themselves, and I think it's OK for exceptions to slow down a program in this case.
But would those 0-cost exceptions be much of a burden in a normal run of a program?
I don't see how checking for thousands of missing file references (or anything similar) is representative of what a program does in normal runs.
IIRC the Eclipse case was similar, checking for optional plugin metadata files or something like that.
A REST API library might throw an exception for a 404. But even then it's debatable, because network errors are very common, so they're a poor fit for exceptions. Networks are laggy, so they're also a poor fit for synchronous APIs. Many procedural programming techniques are on shaky ground when they try to paper over the network.
Now without exceptions, you still have an existence check AND have an error-encoding result on open that is rarely in failure mode. Very annoying. You must combine the two calls even if separating them makes more sense.
I get it that Rust is designed for the command line where that doesn't really occur.
In any case, what you're talking about is hardly the common case to optimize for. In basically every language I've ever used, the majority of files were opened without any user interaction, in which case Rust's API is absolutely the correct approach.
That is not to say that there aren't times where you have to do a redundant check, but generally speaking they only occur when you have control over the entire system under consideration and statically know (perhaps can even prove) that the error can't happen. In that situation, you should of course unwrap(), as there is no good way to recover from a logic bug in the existing program.
Exactly. It is probably still there, just someone could have deleted it in the few seconds or so that it took for the user to make an action. UIs are filled with a bunch of invariants that are probably true but not necessarily so. 99% of these programs will just exception out if say that file is deleted between the non-atomic time it takes to do verification and action. And there is nothing considered very wrong with that: you screw with the environment of an application while its running, bad things are expected to happen.
Now, a language like Rust expects that everything is atomic in the normal case. But since users aren't very batchy, that means re-verifying things over and over again. It is not the overhead that is the problem here, the fact that the check is redundant is not the issue. The fact that the programmer is pestered into handling these cases is a huge issue: Java/C# will just exception out and that's the end of it (you don't really want to bother trying to handle that file being deleted in the few seconds it took for the user to make a decision, or if you deal with it, deal with it at a very coarse granularity).
Rust is not designed for writing interactive programs, it is designed for batchy systems code where "files are opened without any user interaction." But let's not discount why existing languages that are designed for writing user facing code make the decisions that they made.
> 99% of these programs will just exception out if say that file is deleted between the non-atomic time it takes to do verification and action. And there is nothing considered very wrong with that: you screw with the environment of an application while its running, bad things are expected to happen.
I would much rather strive to always do the right thing, if possible, because I do consider that wrong. The pervasiveness of the `Result` type in Rust makes it much easier for me to do so; the cost to you is that you must write `unwrap()` instead of nothing. On the other hand, if unchecked exceptions are the error handling default, I have no recourse in order to discover what possible errors can occur from a function call other than to read the function's source code, while you just save typing `unwrap()`. To me, Rust's solution seems like an eminently reasonable tradeoff.
I've specialized in very interactive systems so my world is quite messy. When you run a compiler (and the program being compiled) while the user is editing it, there are lots of transient error conditions to deal with. I just couldn't imagine writing such a system in Rust, error propagation and resolution is a much more global affair that can't tunnel explicitly through function signatures. But then, I understand that Rust wasn't designed for my problems.
let i = try int s with _ -> -1
OCaml does some similar things, I think. You might use it to attempt to get an item from a dictionary, and return a default if not found. It's easy and elegant.Saying "exceptions are for exceptional cases" is meaningless wordplay. Call them faults, traps, errjumps, whatever and it breaks down (faults are for faulty cases?). Library design should trump all, and sometimes the better way is to throw in many cases.
My advocacy of exceptions for exceptional situations is a cultural one, and the primary aim of the culture is so that you don't catch exceptions. You should almost never catch them. They should almost always bubble up. Only the server handler dispatch or UI event loop should catch, most of the time.
And you can only make that work if the library doesn't throw exceptions for situations where you are likely to want to handle them.
I tend to think of errors as falling into three classifications: (1) incorrect user input, (2) unexpected external condition revealed through a system call, and (3) programming error.
You almost never want to catch (3) - null pointer exceptions, overflow, division by zero, index out of bounds, etc. In some apps, you probably want to terminate. You don't want to catch the exception, you don't want to catch, wrap and throw the exception, because these just obscure the nature of the problem.
You might occasionally want to catch (2) to perform a retry or use an alternative strategy. Depending on how expected the error is, it might make sense to have two different styles of invoking the operation, one that throws, one that doesn't throw. Some code just cares about the happy path and doesn't mind blowing up on failure, other code needs to try harder.
But (1), user error, requires design. It means the user - normally the guy who, however indirectly, is paying for the code - has made a mistake, and that's probably our fault; we didn't do enough communication. We need to work hard to correct it. Exceptions come with technical messages. Technical messages are not user friendly. Throwing an exception would just mean we'd have to catch and translate it. It creates work. It doesn't help. Transporting the message up the stack automatically doesn't help either; context that could communicate the problem to the user is lost. Exceptions simply aren't the right tool. The affordances are wrong, automatic unwind is wrong, and the instincts they inculcate about catching, break everything else.
(1) Exceptions can make your contract with your calling code nicer by offering an alternative way of unrolling the call stack. That way, your caller can deal with anything unexpected in a more convenient way.
(2) But the price of being able to do so is expensive. The whole system is designed and optimized for "normal" procedure calls making normal procedure calls and returning normally. Exceptions go against that grain and are very expensive. So only use them for "truly exceptional" situations.
The problem is, if you try to take advantage of #1, you run afoul of # 2.
TryParse vs Parse is a perfect example. The calling contract of Parse is so much cleaner because it takes advantage of # 1. But having an exception occur is too heavy because it runs afoul of # 2.
Problem is, an exception is almost _always_ too heavy. So it is an illusion (delusion) to think you can ever freely enjoy the benefits of # 1. If you need a guideline like # 2 at all, then # 1 is a lie.
I'm a much bigger fan of Delphi's exception model, for example. You would not normally see any exceptions on an app startup, not least because the default setting in the debugger is to break on exception and it would drive the developer up the walls.
Opening a file should optionally return a file, and if it didn't work return an error value instead. This can be solved with multiple return values (like Go), or an easy to use option type (like Rust). Even C solves this by returning NULL and setting the global errno. But since Java doesn't have either, it uses exceptions for completely mundane cases which then makes their performance much more difficult to tune since they're everywhere.
Can you describe that scenario in more detail? What happens in case of panic?
> I remember stories of Eclipse taking 30 seconds to start up when compiled with GCJ (which used zero-cost exceptions) because it throws thousands of exceptions while starting.
I guess many Java developers assume that it's cheap to throw exceptions, and GCJ can't easily change that. In a new language, you wouldn't have that problem. If you had zero-cost exceptions in Rust, and told people that they have the same performance characteristics as C++ exceptions, I think most people would understand.
Process abort.
> I guess many Java developers assume that it's cheap to throw exceptions, and GCJ can't easily change that. In a new language, you wouldn't have that problem.
No, as I described below, you still do if the core library APIs throw exceptions on error. You have no choice but to catch exceptions if you want to do simple things that programs need to do.
In OCaml (a language with blazingly fast exceptions), the worst that can happen for each try-block is a register spill or memory store [1]. For a managed language, that's excellent.
The only thing I don't like about OCaml exceptions is they are unchecked -- which is strangely out-of-place, given the ML emphasis on type safety.
The resistance towards checked exceptions doesn't make sense, either: used properly, checked exceptions could be virtually indistinguishable from typechecked return values. They also enable some really elegant control-flow constructs, although that may take some discipline to use properly in a large project.
[1] http://stackoverflow.com/questions/8564025/ocaml-internals-e...
OK, but that's OCaml, a language with no value types (other than 31-bit ints) or RAII. So you don't need custom cleanup code in particular, since the runtime tagging tells you everything you need. That isn't an acceptable approach for Rust, which uses unboxed types with custom cleanup code everywhere. You would need an approach like the approach Go uses to implement defer, which involves pushing and popping custom handlers onto a linked list all the time.
> The resistance towards checked exceptions doesn't make sense, either: used properly, checked exceptions could be virtually indistinguishable from typechecked return values. They also enable some really elegant control-flow constructs, although that may take some discipline to use properly in a large project.
We could have used checked exceptions, I guess, as they're isomorphic to ADTs, but I don't think there's much of a benefit when checked exceptions still require you to annotate signatures or catch the exception. Also, without subtyping of exceptions, the annotation burden would likely be worse. And the automatic wrapping and unwrapping of values in exception types would probably make systems programmers unhappy that extra indirections and data structures are being conjured up in ways that aren't immediately obvious.
Finally, checked exceptions have this annoying property whereby the easiest thing to do to handle them is to write "try { ... } catch (Exception e) {}", and programmers often pick the easiest thing to do. Compare to Rust's ".unwrap()", which is does the sensible thing if you don't want to bubble the exceptions up to your caller, and is much less typing.
Hence the "managed language". Such features would certainly be more difficult to do in Rust, and agreed that exceptions probably aren't the ideal error-handling mechanism given the goals of Rust.
I guess the point was that exception tradeoffs can be mostly overcome by a well-designed implementation, given the appropriate language. (Not saying other languages have bad implementations, although the entire OCaml runtime is a work of art that's very nice in more ways than just exceptions...)
> checked exceptions still require you to annotate signatures or catch the exception
Could the signatures be inferred with the exception type(s)? Not catching them at some point in the call stack would result in a compile error; no reason it has to be at the caller. Probably tricky for libraries, maybe having a notion of scope for exceptions would be useful. Although I'll admit I don't know the first thing about the nuances of type inference.
> the automatic wrapping and unwrapping of values in exception types
I could be missing something, but how would this be worse than one or maybe two dereferences at catch-site(s) and nothing in between? Is the problem with exceptions containing large unboxed values that have to be propagated up the call stack? Since call stacks can be arbitrarily long, this seems like another good reason to limit the "scope" for exceptions somehow.
> easiest thing to do to handle them is to write "try { ... } catch (Exception e) {}"
True, generic pattern matching has the same problem with the 'default' case (eg, match _ -> ... ), that's often a code smell. Moreover, the notion of a syntactic try-block seems like something that a "sufficiently smart" compiler could do away with, given enough inference: just match on the "returned" exceptional values [1]. But then again, what do I know about type inference...
[1] OCaml has a pretty nice feature which approximates this: https://blogs.janestreet.com/pattern-matching-and-exception-...
Can you register a global panic handler in Rust apps yet? Because that's usually what you want to do in fail-fast scenarios for error logging etc.
If instead the result of the operations are fully encoded into the return value, then there is no problem with how to type map, fold etc., because results are all handled uniformly by the accumulator (for fold) or the transformation function (with map), etc just any other sorts of values would be.
Map, fold etc. work just fine with typed exceptions if you parameterize map, fold etc. by the exception type. The problem comes with runtime polymorphism; runtime polymorphism is a firewall to static type flow. Runtime polymorphism is also fairly popular with Java developers.
(Note that e.g. a monadic map is implicitly parameterized by the "exception" type via a type argument of the return type (Either, Result, whatever you like) of the transform passed in.)
Well that's rather in line with what I meant by "don't work very well".
BTW, do you know of any languages/libraries that chose to define map/fold (or similar) to be parameterized over exception types like this? That sounds interesting enough that I'd like to see examples of what it would look like in practice.
Can you elaborate?
The only problem I can see is if the language forces the exception to be handled by the caller. That doesn't have to be the case: the language could require that the exception be handled somewhere in the call stack, which would fix this problem.
Kernel developers is another good example.
__copy_from_user feels very awkward by comparison.
I disagree with the GP about whether Rust's error system is elegant, but syntax isn't that tightly bound to codegen. Indeed, I'd be interested to see an optional pass someday that does the opposite, converting Result returns to exceptions at the LLVM IR level.
That's what I mean by overhead for even the non-throwing case.
> - or at least, all calls that can throw, a flag which can be either specified in declarations of stored in metadata, though not without some complexity in the latter case in order to handle separate compilation correctly.
That's checked exceptions, and I don't think they'd be a good fit for Rust for a few reasons that I've outlined in comments downthread, but briefly: (a) the annotation burden would be just as bad; (b) automatic unwrapping and wrapping of function result values could be surprising for systems programmers; (c) checked exceptions like Java don't allow for the nice .unwrap() syntax; (d) checked exceptions, as an out-of-band effect type, don't play as well with higher-order functions as just using the normal return value. But yes, it is possible.
They shouldn't be that expensive. It'd help if libgcc didn't take a mutex around the exception dispatch (because of the very unlikely possibility of a concurrent dlclose) so that different threads could do exception lookups in parallel.
(I once tried replacing that mutex with an rwlock. It didn't help very much.)
C++ zero-cost exceptions make successful code run fast--faster than the equivalent successful C code--and in the cases where the compiler infrastructure (or more likely in 2015, cultural baggage, as it really doesn't take much effort to bootstrap exceptions) or memory constraints prevents them from being used, the great thing about the C++ mindset is that they don't cost you anything to ignore: even in a program that has exceptions elsewhere, if your code doesn't need unwind semantics (and a lot of code doesn't), you don't take any overhead; and, as kernel and game developers also often build their own data structures--not to avoid exceptions, but for other important pragmatic reasons (including but not limited to virtual memory restrictions, control over non-determinism, and extreme case-specific optimization)--it is easy (and, I would argue, strongly preferable) to free most developers from frustrating error handling boilerplate while still seeing (as we do) the developers who somehow need to avoid these features having no issues doing so (as we do in the C++ community).
Did you read the OP? I realize the post is long, but the short story is that error handling in Rust can be concise and ergonomic. It's only verbose if you're doing explicit case analysis every where. I've found error handling in Rust to be the exact opposite of frustrating. It's pleasant and explicit.
I don't know Java or C++, so I can't help you there.
> I consider having to unwrap or try! on essentially every function call
They aren't on every function call. Only on functions that can return errors. The type signatures will tell you which.
> to be verbose.
I don't. In fact, I love how errors are handled. It's explicit, concise and ergonomic. We'll just have to disagree, I guess.
Rust programs are littered with calls to unwrap and expansions of try! for the sole purpose of doing manual error propagation, which is both verbose in the code and slow at runtime (as the successful case must constantly produce and then check error results), just so the error can get to the top level somewhere and be reported to someone (which in turn drags a surprisingly large amount of your code into the domain of "functions that can return errors"). None of that boilerplate should be required: even with an insistence on the mechanism, it is such a clear case of boilerplate that a friend of mine finally ended up working on a compiler plug-in whose purpose was to jusy add calls to try! around anything that seemed to require those calls.
As I said, we'll just have to disagree.
> My advice boils down to this: use good judgment. There’s a reason why the words “never do X” or “Y is considered harmful” don’t appear in my writing. There are trade offs to all things, and it is up to you as the programmer to determine what is acceptable for your use cases. My goal is only to help you evaluate trade offs as accurately as possible.
Having to litter code with unwrap and try! just because somewhere much lower an error is used and much higher an error is reported does not seem beneficial unless the alternative is to do it in a way that is even more manual and less safe, but that's not how it works in the languages I use on a daily basis. Yes: C and Go make dealing with errors so painful and dangerous that Rust is absolutely amazing in comparison... but I don't program in C or Go and I would hope we aren't using them as the benchmark when we have C++.
I encourage you to read my example with type declaration that I wrote elsewhere in this thread.
See pcwalton's comments in this thread on exceptions.
Your argument that you do not want error propagation to be implicit is strange to me, as a developer who is used to this being automatic, because it is does not affect safety to automate and is effectively nothing but boilerplate. There is nothing different in the form of your argument against exceptions from the form of the arguments made by developers of C or Go or Java against adding features like templates or optionals or macros. It isn't that Java developers hate abstraction in all forms (that would be an absurd argument that I would need to pull from an AbsurdArgumentFactory ;P), it is that the form of argument that we often hear from all of these developers about the features we like in languages like C++ or Rust or D has the same core structure :(.
On pcwalton's notes on exceptions, I already responded to him in my comment that started my involvement in this thread, and went further in my first reply this morning: his mental model seems stuck in the implementation of exceptions as defined by Java and as used by Java developers. His datapoint for why they are so slow is from an old story about gcj: the Java exception model requires a stacktrace reification, and you would not be able to replicate that kind of performance loss in C++, even though they share the same implementation of propagation. His API issues are all (as argued by others) based on the awkward way that Java developers have made exceptions used for things that aren't really errors.
Have you developed in Erlang? This is a language that considers errors and failures to be the core problem faced by developers, and which has structured almost all other decisions surrounding that core premise. After spending a lot of time coding in Erlang a few years ago, it fundamentally changed the way I think about errors, and that was after over 20 years of software development in languages like C++ and Java. When I was (comparatively) green and naive, back in 2001, I even made an argument for checked exceptions to Microsoft in C# (it was partly my argument for RAII in that same thread that got us the using syntax sugar for IDisposable): that is how far I had to come in the subsequent decade.
Erlang has the right abstraction: a mechanism for exceptions that is designed somewhat similarly to panic, in that it encourages you to structure your code in ways that isolate failures behind processes and avoid ever "handling" errors, but which lets you propagate and augment rich error context through multiple process layers implicitly, making error propgation automatic and without boilerplate. Once you "get it", you can do this easily in any language that has unchecked exceptions (even Java, using RuntimeException as the new exception root, but you have to really hate Java to pull his off ;P), as function calls can be mentally modeled as calls through process linkages (though you sadly then can't use the standard libraries provided by any of these languages, as they are all built with a broken model :().
Given how many other amazing things that Rust got uniquely right, it is somewhere between disappointing and devastating that error propagation, something which is so important, only seems to be competing with languages (like C, Go, or Java) where errors are almost unworkable :(.
Yes.
> Erlang has the right abstraction
There is no right abstraction. If we can't minimally agree that there are trade offs at play here, then we can't have a productive conversation.
> and avoid ever "handling" errors
I want to explicitly handle my errors.
Just because you had great internal progress does not mean you arrived at some objectively correct solution. It obviously works well for you, but everyone is not you and not everyone has the same requirements or preferences as you.
My argument is "given that there is absolutely no demonstrated benefit to doing so, and even some clear downsides (performance of normal successful code), being forced to litter my code with tons of boilerplate--something you can clearly see if you glance almost anywhere in the code for Cargo--is a major and depressing step backwards". Your argument seems to still be "I don't want that much automation, I like things being explicit, and of course you should look again the comments from pcwalton you already responded to that complain about Java (really just Java) being slow"... :/.
Honestly, please just leave me alone and stop twisting my words. We can't even agree that there are trade offs involved here. There's no point in continuing.
I wish you would be more realistic about the tradeoffs of exceptions, though. The win in compile time alone from not having landing pads everywhere is substantial, and the "error handling boilerplate" you discuss isn't generally agreed to be boilerplate.
I also maintain that requiring the developer to do manual error propagation to get errors from the point of failure to the point of reporting to the administrator or the operator (which is what happens to almost all errors, whether you are writing a command line tool, a graphical application, or a website) is "boilerplate". At the simplest, without changing the error model, littering the code with calls to try! that push the error upwards is automatable (and I have a friend working on a compiler plugin to accomplish exactly that).
An example: I like statically-typed languages, but as demonstrated by languages with type infererence, having to declare every type every time it is used is "boilerplate", especially if types are long and unwieldy. C++98 has typedef, which lets me take C++'s long and unwieldy type definitions and make them shorter. If you are used to typing them out manually, every single time, some might even claim this is "ergonomic". However, C++11's auto is much better, and as demonstrated by languages like OCaml and Haskell even that isn't as good as it can get. C and Go have long and unwieldy error propagation (checking flags and manually returning new ones). Rust provides macros and tooling for this that make it safer (no more "goto fail;" or "Rage Against the Cage" exploits) and easier (like typedef!), but that doesn't mean it isn't still "boilerplate".
I don't consider the error handling boilerplate because I do frequently want to recover, even from supposedly fatal errors. For example, if I'm handling an iterator of things, and some of them fail, I still usually want to keep processing them before I return the error. Exceptions lock you into a very rigid pattern of error reporting that make that much more difficult to deal with. Because libraries don't assume that I want to fail when they do, it can be easy and fast for me to do this, even for pathological inputs; that wouldn't be the case if everyone used exceptions.
I agree with you that Rust could do better at providing options for error reporting with panic. I believe there's an RFC about that right now, please comment on it!
Solving problems like your iterator issue should not be difficult with exceptions: in fact, frankly, I don't see why it is any harder than with Result: your worst case is that you start with a primitive, "antitry!" (which should be called "try!", as it makes more sense than what "try!" currently does for that word) which performs an operation and returns a Result of either its value or its failure. Even if you do this "frequently", I would be shocked if this was remotely as common as the code that was assuming success and just wanted to fail if something fails. And, as long as you aren't trying to "handle" those errors (and it doesn't sound like you are, you just kind of want to fail out with a list of failure reasons) there is no semantic complexity either. Exceptions are the flexible primitive, not Result: simulating Result from exceptions requires a tiny finite amount of code, while simulating exceptions using Result requires modifications to an arbitrarily large amount of code.
As for fixing panic, what really sucks is that so much library code is going to be written using Result now when much of it should have been written with panic, both in the core library and from third-party developers, meaning that the boilerplate is going to be somewhat inescapable at the lowest levels of the program... I think the thing I like most about programming in Python vs C++ is that I don't have to wrap every single API I use to make it handle errors in a sane way. I don't think that fixing panic at some point in the future is going to work as there seems to be such awkward community momentum against exceptions and behind Result, when the real RFC needs to be "when you write your library, try to use panic, not Result".
Um, actual, non-trollish question: do you know how exceptions are implemented? Because I can't think of a way around this in a language with destructors, short of some sort of "doesn't throw" effect (which Rust doesn't currently have) or aborting on every exception. Do you have any pointers? Similarly, I don't understand how your "antitry" solution would be efficient on pathological input--"zero-cost" exceptions are far from that when the exceptions actually happen (and can be made to happen by malicious users). You also haven't addressed how I'm supposed to know to use antitry in the first place, which is one of my biggest issues with unchecked exceptions--again, the only alternative I see is a "throws" effect. All of these are language level concerns, not implementation-level ones.
> Exceptions are the flexible primitive, not Result
Catchable exceptions require coding transactionally if you want to use them everywhere and maintain strong exception safey. There's an enormous cost associated with that too; and worse, it can't be checked by the compiler.
Again: I have actually benchmarked a lot of code that uses Result, explicitly checking to see if it was a performance issue. It's almost never a performance issue. So performance-related arguments for why exceptions should be used everywhere are going to fall on deaf ears here. I am far more interested in seeing whether we can eliminate panics altogether for most of the common cases (e.g. with compile-time checked range types).
Rust does follow the model of "exceptions for truly exceptional cases"--that's what panic is for. It's implemented with unwinding under the hood, just as in C++, and you can change it to abort if you want.
When I have seen Rust code, it is always littered with error propagation logic, which seems to be due to a failure by language designers to realize that most use cases for errors should be using a mechanism like panic, in turn leading to panic being pretty useless: as it can only throw a string, it is impossible to provide the kind of rich context that would be needed 50 levels up the call stack (maybe all the way up in main) to report the error to the user (which ranges from information that is description to that which is technical). This is what most, and I would argue almost all (but going this far seems to require an hour of your time and a whiteboard, as people have been so trained to try to handle their errors constantly :/), use cases for errors are: to store and provide rich context on the error, whether in a log for an administrator or for the operator.
This is what a command line tool does with almost all errors, this is what a graphical application does with almost all errors, and this is what a website does with almost all errors. So while it would be great to try to sell someone on using panic, as in reality almost all errors in their program should be using panic, that means that all those errors have to, specifically at the point of the error or very near to it, be translated all the way down to a useless string... this will remain in the code for maybe a day before you sigh and get angry at how rigid your error reporting is and in the process of fixing it demand 1) more structured context or 2) the ability to annotate the error with more context at a few key places on the call stack.
To do either of these things in Rust requires that you switch to the painfully verbose (and unfortunately slow!) case-based error handling mechanism, which then forces you to throw down unwrap calls and try! expansions everywhere in your program even if you have very little logic that is even capable of failing in any normal sense, as error propagation in Rust in manual :/. I would love to use panic in my website or tool, and "omg this failed in a way I can't recover" is absolutely the semantics I want for all of my errors, but as far as I've been able to see--even in code from Yehuda, who sadly moved to Portland, making it difficult for me to sit down with him at a whiteboard for an hour :/--to be able to report (not handle: I don't want to handle panic, just report it) these errors in any reasonable way always leads to not being able to use panic, despite it being the more correct tool.
Wow, I have exactly the opposite impression. I am so excited by how elegantly error handling is done in Rust.
> Having to write all that error handling code up front is going to be a big problem for the Agile crowd.
I really disagree. At my last job (Pivotal Labs, definitely part of the 'Agile crowd'), large internal projects have been ported from Ruby to Go. There are a lot of things I don't like about Go, but one thing that everyone I worked with really enjoyed about it was the clarity and explicitness of the error handling. Exceptions are hard to reason about and hard to remember to handle; errors returned from functions are so simple. It seems like Rust's error handling has the good properties of Go's, but it allows for more abstraction because of Rust's more powerful type system. I think the Agile crowd that I've hung out with is going to love it.
Meanwhile, apparently the Rust folks can't make up their minds, so they support lots of different functional styles of error handling.
In other words, there is no way to abstract over the case analysis in Go. It sounds like you're saying this is a good thing. I think it's bad.
> ... apparently the Rust folks can't make up their minds
Wow, I'd say the opposite. In Go, the only thing all `error` objects have in common is the `Error()` method, which returns a description. If you want to investigate the causal chain of an error (to achieve what stack-traces give you in languages with exceptions), there is no standard way, even for errors in the standard library. Some errors are basically just strings, others are structs with other fields that you can access by using runtime type assertions.
Rust, on the other hand, has the `Error` trait, which includes a `cause` method, returning the underlying error if one exists. This alone seems to me like an improvement in consistency over Go.
But I don't think abstracting over case analysis is a good idea when it hurts readability. The combinator stuff looks like it's adding layers of shortcut notations that obscure the control flow.
(It also abstracts over case analysis, among other things, but it's only one very simple form of case analysis, so the mental overhead is low. Combinators on the other hand abstract over many different types of case analysis, so the mental burden can be high, especially if you aren't already comfortable using them.)
The API signature is always the same regardless of how the caller handles errors: a function returning Result. The differences are never exposed in APIs (arguably even less than in Go, since you can't recover from a panic in Rust except at thread boundaries).
> Meanwhile, apparently the Rust folks can't make up their minds, so they support lots of different functional styles of error handling.
They all boil down to the same thing. There's no inability to make up our minds with error handling any more than supporting both for-loops and map represents inability to make up our minds. Nobody dings Python for supporting list comprehensions in addition to for loops. Sometimes functional programming is short and sweet, and you should be able to use it; other times, for loops are better. and_then() is just the functional syntax to "try!"'s imperative syntax.
It seems like having lots of trivial error combinators is less like Python's list comprehensions and more like what came before it. I'll point out that Guido has had second thoughts about lambda, reduce(), filter() and map(), though in the end he only dropped reduce. [1].
But still wonder how much of an anti-pattern that would be in Rust -- spawn a separate thread, and then do all the IO, HTTP, XML, RSS parsing in a clean way. Then let it panic and somehow find the reason for panic in the original main thread. Then decide there what to do (exit, log, bubble up failure etc).
This lets it handle failures that panic, and then shrink the witnesses to find (hopefully) minimal witnesses. :-)
That said, there is a `catch_panic` function (http://doc.rust-lang.org/std/thread/fn.catch_panic.html), but it's unstable. So in stable Rust, the only way to catch a panic is to spawn a new thread, I think.
Rust's error handling can be handled in a much cleaner and more expressive way than exceptions are handled and they are fully integrated into the type system. I don't know what advantages exceptions have.
The 'and_then' is a continuation for the happy case, the 'or_else' is a continuation for the catch block. From my reading of the article, there is a mechanical transformation from exception-handling code to the continuation passing compositional style advocated.
Checked exceptions have their problems - and I'm unconvinced on first reading that Rust has not got the same compositional problems without a whole bunch of extra complexity in parametric polymorphism - but the real problem is that most code shouldn't be dealing with errors low down in the stack. Code that makes the happy case verbose and forces you to continuously deal with the error case promotes either of two things: logging and then ignoring errors at a low level, where callers can't control the decisions; or Herculean efforts to transport errors up from deeply nested stacks all the way out, so that error passing code starts to dominate over code that works for a living.
> I wrote an RSS reader in Rust, and all the code required to compose all the error types (I/O, HTTP, XML, RSS, date formats) into a custom error type is as bulky as the modest amount of code that does the useful work....
If the errors are internal implementation details that you don't want to expose (or you just don't feel like writing wrappers), and you don't want to handle them either, you can return `Result<T, Box<Error>`, as noted in the article. This requires all of no work on your part as long as other libraries are also returning instances of `Error` (and in general, they are), while also ensuring that your library returns an instance of `Error`.
If your library defines its own meaningful errors, and you want to expose those to the user, then you will need to create a type and define `Error` for them, but you would also have to do the equivalent in any typed language with exceptions. If you additionally want them to be "top level" errors, but want to wrap other types of errors as the ultimate cause, you'll also need a single `From<Error>` implementation (from generic errors to your type) which in my experience is not even more code than a single catch-and-rethrow-as-another-type in a language with exceptions.
(I bring this up mostly for the benefit of others, as it has been explained to you before. Perhaps when you wrote your library `Error` hadn't been stabilized yet, but all of what I said above has been possible in Rust for many months now).
> Having to write all that error handling code up front is going to be a big problem for the Agile crowd. This may be a major drag on Rust adoption.
The "Agile" approach to error handling is `unwrap()`. It's not recommended in libraries because they violate the contracts the libraries expose, a problem they share with unchecked exceptions in other languages.
> All this error machinery is in Rust to avoid exceptions. That may have been a mistake.
Preferring sum types is not just true in Rust, where one might speculate that it's because it lacks `catch`: my impression is that in all three of Haskell, Scala, and OCaml, which support both, public opinion has moved steadily away from exceptions and towards `Either` for error handling. This is quite striking given that there are good reasons to believe that exceptions would be popular in those languages: in Haskell because pure code suffers from relatively fewer exception safety issues, in OCaml because the exception implementation is highly optimized, in Scala because the prevailing ecosystem (where most of the employees are drawn from) uses exceptions for error handling. The fact that all of these communities, given both options, tend to use sum types, suggests to me that the theoretical benefits of this approach (many of which you covered) are also practical for many people.
> Exceptions have the good property that they can't be ignored - if they're not handled, you get a reasonable program termination.
In a recent paper, the following three things were found to have led to 35% catastrophic production failures: https://www.usenix.org/system/files/conference/osdi14/osdi14...:
> (i)the error handler ignores explicit errors; (ii) the error handler over-catches an exception and aborts the system; and(iii) the error handler contains “TODO” or “FIXME” in the comment.
It sounds like people do a fine job of ignoring exceptions. Moreover, it seems to me like what you are advocating (catching `RuntimeError` or something equally generic, where you can't possibly take an explicit corrective action, but not terminating the program) is precisely what ended up causing a lot of these bugs.
In any case, it's not likely that you'll out and out forget to handle a `Result` in Rust. You can't even try to access the inner value without handling errors, and if you just don't use it (because you don't care about the value) Rust generates a warning (people generally pay attention to Rust warnings, since there are relatively few of them). If you `unwrap()` it, it's even easier to find where you aren't handling your errors properly--you just grep for `unwrap()`.
In my experience, people ignore errors far less in Rust than in any other language I've worked with, because it actually makes them explicit when they happen, rather than propagating them down to a consumer who has no idea what to do with the error.
> If there's a reasonable exception hierarchy, you can catch an exception near the root of the tree and get all subsidiary exceptions.
I also am interested to hear why you believe that hierarchical error types make sense: errors seem to me to be one of the absolute worst fits for the classical inheritance model. They very rarely have any shared behavior or state that isn't also on `Error`, and trying to fit them into a hierarchy is an exercise in frustration that is undertaken for the sole purpose of avoiding multiple catch clauses--they very rarely compose and are even more rarely extensible. More often, people just give up and use a single type with a String, or--equally commonly--simply `catch Exception` whenever they expect an exceptional condition to be raised without bothering to check the type at all.
> you have to combine error types yourself.
And coming full circle... no, you don't :)
> I really want Rust to succeed, but I think they took a wrong turn with error handling.
You have repeated both this sentiment and the bulk of your comment in many, many posts now. I believe that you are sincere in your desire for exceptions (even if I disagree with it), but I don't think you or anyone else seriously believes that not making catchable exceptions the default error handling technique is going to make or break Rust adoption, especially when its primary target audience is known for routinely disabling exceptions to avoid dealing with exception safety and performance issues.
Meh. Agile or robust; sometimes people have to choose.
Under this use-case, the verbosity is still a pain, but it's not a major drag on adoption. You already know what you're building and have something that basically works, just slowly, so you don't have to pay the cost over and over again in product iterations. And your expectation isn't "Oh, I'm just trying something out, why can't it let me get to the fun part?", it's "Well, this sucks, but it's what I'm being paid for and at least I'm solving a hard technical problem with the most performant code I can."
Next week, burntsushi and I are going to talk about rolling this into the official documentation.
fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
let mut file = File::open(file_path);
let mut contents = String::new();
let read_Result = file.read_to_string(&mut contents);
let n = contents.trim.parse::<i32>();
let result = 2 * n;
error_handling {
file: None => Err("Failed open file"),
read_Result: Err(err) => Err(err),
n: Err(err) => Err(err),
_success: Ok(result)
};
}
The compiler would then convert each error handling case into early return statements in the places where assignment is made to the specified variable.I personally like the style that `try!` encourages.
...
argv.nth(1)
.ok_or("Please give at least one argument".into())
.and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
...
What is str::into? I find the documentation is really hard to navigate for a Rust newbie like me: here's what I've tried:1) Search "into" on the API docs. I find (https://doc.rust-lang.org/std/convert/trait.Into.html#tymeth...) and see that str implements this trait to convert it to a Vec<u8>. Not seeing the need for a conversion to Vec<u8> I...
2) Search "ok_or": I find (https://doc.rust-lang.org/core/option/enum.Option.html#metho...) but I still don't see why the string has to be converted to Vec<u8>.
3) Maybe literal strings are Vec<u8> and into() just is used to convert a Vec<u8> to str. Can't find any documentation on that, and seems fishy if it were the case.
4) Try googling "rust str into" with useless results. Not Rust's fault here :)
I haven't used Rust yet but often try to read other people code and understand what's going on, and everytime I hit the API docs I'm filled with despair that I can't find what a function is supposed to do.
Again, I'm a noob. I must be doing something wrong..
error: mismatched types:
expected `core::result::Result<_, &str>`,
found `core::result::Result<i32, collections::string::String>`
(expected &-ptr,
found struct `collections::string::String`) [E0308]
hello.rs:6 .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
hello.rs:6:25: 6:74 help: pass `--explain E0308` to see a detailed explanation
In other words, without the call to `into()`, we expected a &str, but got a String. This `into()` call is the first one that you found, but, through some magic, it's not clearly obvious, it's true. The secret is Into's opposite, From: https://doc.rust-lang.org/std/convert/trait.From.htmlThere's a generic implementation in the standard library:
impl<T, U> Into<U> for T where U: From<T> {
In other words, anything that implements From, implements Into as well. So generally, things just implement From, and take advantage of this blanket implementation.With this knowledge, this From implementation makes sense:
impl<'a> From<&'a str> for String
We can convert From a String to a &str, and hence, we can convert a &str Into a String.This kind of type shenanigans is _very_ powerful, but admittedly, a bit confusing.
"foo".into()
could be expressed as String::from("foo")
I would personally use the latter form as it is easier to parse and reason about. Thanks for the explanation!Furthermore, there's a non-generic version of this particular conversion:
"foo".to_string()
And, a generic version that expresses a different semantic: "foo".to_owned()
Strings are a very common type, and so implement a _lot_ of these things, even when this kind of usecase may overlap.I can't say this is very helpful; it's hard enough learning the one way. Why have `to_string()` if we have `into()`?
Please don't think this way! Steve already answered the technical question brilliantly, but the Rust docs can be very difficult for newcomers to navigate. I don't think we've found a really good way of making unknown unknowns easily discoverable. The `into` example here, for a new comer, is particular is tricky because it requires reasoning about very generic conversion traits. It's hard to know that without, well, knowing it!
So if you have a function x that has some unhandled "exception" thrown, what it does is it returns an error value at that point to the caller (instead of actually throwing an Exception).
Does that exist?
On the other hand, if it's not universal then you have to set boundaries where the exception to value transition occurs and this is "just" a catch block.
I was thinking something like you get an Optional, and you check it.
If you use it without a check (e.g. something like "try" in Rust), and it's an error value, then it "explodes" again and bubbles up.
EDIT: Oh, I see, you mean without having to declare it.
Consider function f() which calls function g() which calls function h(). So the call stack is f -> g -> h.
Suppose that h() returns to g() with an error condition. Per your description, it now returns to f() with the error condition, rather than continuing. f() in turn returns with the error condition, because the function it called had an error.
The only way I can read your suggestion is a distinction between errors caused during a subroutine call, and errors caused directly in the body of the current function (like a divide by zero or something). But these errors are quite rare; most code is composed of calls to other code. So I'm not sure what error you could have that causes immediate return which is also distinct from a the error-case for return value of the function.
In the handling, something you catch vs something you check the return value for (e.g. with an Optional).
So, it's like an (implicit) try/catch block, where the catch just returns an error value.
https://en.wikipedia.org/wiki/Icon_%28programming_language%2...
It took me a couple of minutes to figure out that this means: "you can have heterogeneous collections of errors, and you can use them 'polymorphically' without knowing their concrete type."
In other words, you can use Error as a "trait object." I don't think a newbie to Rust would follow the language in the article here.
(Just a suggestion to help improve an outstanding article).
io-basic-error-custom
and the first version of error-impl
have the same content but different names, while the reference just above the first version of the latter points to the former.Is this intentional? It confused me for a second.
I know this is nitpicking, just asking.
And, thank you steveklabnik, pcwalton, and other contributors of Rust for working in the open and answering our questions here. Thank you burntsushi for your great post.
error-impl: https://github.com/BurntSushi/blog/blob/master/code/rust-err...
io-basic-error-custom: https://github.com/BurntSushi/blog/blob/master/code/rust-err...
The key is that the code is spread out over multiple blocks in the article. I need a better literate programming tool than hacky Python...
Maybe it would be interesting for the reader to see the number of the lines of the shown code, something like "error-impl.rs, lines 3-12 out of 51".
:P
fn double_number(number_str: &str) -> Result<i32, ParseIntError> {
match number_str.parse::<i32>() {
Ok(n) => Ok(2 * n),
Err(err) => Err(err),
}
} match number_str.parse::<i32>() {
This matches on the return value of parse, which is an Result<T>. It can have two values: Err(E), or Ok(T). Ok(n) => Ok(2 * n),
This line matches an Ok(T), and binds the value of type T to n, which can then be used on the right hand side. Err(err) => Err(err),
this line matches an Err(T), and binds the value of type T to err, which can be used on the right hand side.The right hand sides re-wrap these values into another Result.
For instance, if you wrote
5 * a = 35
You know exactly what the value of `a` is because that value matches the given pattern.Here, you have an equation that looks like
Ok(n) = number_str.parse::<i32>()
So `n` is defined by the value of `number_str.parse::<i32>()`.