8 ms·
Errors and Exceptions
- TerryADavis 11y agoLinux wants to be a 1970's mainframe. TempleOS wants to be a modern C64. In 1970's they had 100 users on 2 Meg of RAM. Today, nobody would tolerate how slow disk swapping is, if you ever actually rand out of RAM and started swapping.
- Animats 11y agoWell, that's one position on the subject. The Rust people prefer option 3 over option 4. Go takes the same approach. Python prefers exceptions, and the exception hierarchy puts (almost) all the exceptions which result from external problems under EnvironmentError. Much of the trouble with error returns comes from the strange C convention that functions with return values can be called as if they didn't return a value. This is a consequence of the original K&R PDP-11 implementation without function prototypes, and has messed up programmers for four decades now. RAII (resource acquisition is initialization) is popular in C++, but if anything goes wrong in a destructor, things usually come unglued. I/O errors on file close where close is via a destructor tend to be either ignored or fatal. Python "with" clauses handle failure during release better. This was well thought out in Python; exceptions in "with" clause exits are handled rationally. With Javascript, you get none of the above, although you can build it out of the primitives, and there are kludges to do this.
- jblow 11y agoI also prefer option 3 over option 4. It bothers me that the article recommends exceptions without seeming to understand their drawbacks (the major one: as soon as you use exceptions you suddenly need to apply nonlocal reasoning everywhere in order to understand what your program will do at any point. They turn your program from a simple local thing into a complex nonlocal thing, which is not a good idea if you want to understand it well.) This article seems not to have a lot in the way of new contribution; it is just parroting the oft-repeated idea that you need exceptions to pass "error information" up several layers of abstraction. But here is what I think about this: 1) In this age when we are realizing strong typing is a good idea, that hidden state is a bad idea, and that in general you should be very specific about what is going on, why are we even conceptualizing this as "error information"? Why instead, when we try to open a file, do we not return "all information you might need to know in the case of opening a file" (which includes what happened if it didn't open properly). As soon as you make that conceptual switch, all this hand-wringing goes away. It's a non-problem. You certainly shouldn't add heinous complications to your program to solve this non-problem. 1a) This conceptual change also helps disambiguate between what the article calls "hard errors" and "soft errors". In portions of the code where you have attempted an operation that might have failed, and you are not completely sure that it didn't fail, you have the full body of "what happened" information (it is a small struct or whatever). After the situation has been checked and you know it is exactly what you need to be, you may drop the other information and pass the raw file handle. At this point it is clear that these parts of the code should only be executed if the file handle is valid, and if that is not true, the programmer made an error. This is analogous to the situation with nullable and non-nullable pointers (in some languages you would even use the same mechanisms to deal with null pointers and invalid file handles, etc, but I am not sure this is really helpful.) 2) If one insists on not making the simplifying leap from (1), well, maybe the other problem is that you have so many layers. If you didn't have so much glue, your code would be simpler and easier to deal with, and it would run faster, and you wouldn't be so worried about needing to pass lots of context information up several layers between modules, because those situations don't really arise.
- barrkel 11y agoIn this age when we are realizing strong typing is a good idea, that hidden state is a bad idea, and that in general you should be very specific about what is going on Java tried to be typesafe about errors, about not obscuring the state, and it was a dreadful idea. The way code fails is a function of its implementation; communicating high quality error information is fundamentally an abstraction violation, but a strongly typed facade that forces you to rewrap all your error cases in some other type just obscures the underlying problem. Strongly typed error information also breaks composability. How do you write a 'map' function that can accept a callback, where the callback may want to communicate back one of several distinct kinds of error information? With unchecked exceptions, the way forward is clear. Without them, you either need to write a lot of boilerplate to unify your error types in some kind of container, or rely on subtyping to fit them into a single type and live with the lack of strong typing via another route. Dynamically typed programming languages improve usability over statically typed languages particularly when a precise description of the types used in the static case is difficult or tedious to express. Error information, and its intrinsic implementation-dependent content, is one of the strongest instances of this.
- dllthomas 11y agoYou could have checked exceptions with more flexibility than Java had, and it might work out well (or might break down for other reasons). "How do you write a 'map' function that can accept a callback, where the callback may want to communicate back one of several distinct kinds of error?" With Java style checked exceptions, you don't, and that's for sure a problem. However, one approach would be to say that map is polymorphic in the exceptions that may be raised from within it. But that set is not entirely unbounded - it is precisely the set that can be raised by its function argument, unioned with any that might be raised during traversal of its container argument.
- Animats 11y agoExceptions are hard to get right in language design. They're easier in a garbage-collected language like Python, because there's not so much need to release stuff. Most of the bad reputation of exceptions comes from C++, where the combination of exceptions, RAII, and having to release memory results in problems. Without exceptions, programs seem to develop too many "goto" statements. Go suffers in this way. Error handling in Rust seems to excessively complex, and hiding the complexity inside macros that do return statements is an ugly solution to the problem.
- barrkel 11y agoGo's preference for 3 over 4 is a big chunk of the reason I've never used it in anger. Rust's situation is slightly different; code typically uses a monad (Result<T,E>) for error propagation, which is mostly isomorphic to exception throwing, but more verbose. And Rust has macros to cope with some of the verbosity.
- jerf 11y agoIt may be isomorphic to exception handling, but it promotes a meaningfully-different human style. The exception-based isomorphism to the usual returned-error handling style would look something like: try: a_single_statement_here() except: if exception.type == "A": #yada yada elsif exception.type == "B": #yada yada else: raise Exceptions by contrast encourage putting a lot of statements together, and much less frequently checking for what the exception may be, and capturing fewer of them. The code snippet above is bad exception style because you shouldn't be putting a try around every statement like that. Exceptions expressed in the error handling modality probably look something more like: val, err = func() goto EXCEPTION if err nextval, err = func2(val) goto EXCEPTION if err # etc etc, for the whole "try" block return goodval EXCEPTION: { if err.type == "A" { ... } if err.type == "B" { ... } return err } which is, intriguingly, also very bad style for an error-returning language; the EXCEPTION block must be written excessively generically, when in fact the error handling may in fact care about the difference between whether the error came from func or func2, and may even want to take different actions (especially w.r.t. retrying). You can of course write those different actions into an exception block, but the style discourages that... your code starts to look messier (as nice as exceptions can be, they're syntactically pretty heavy, and also difficult to factor out the way that error-return handling can be factored out with monads and such (albeit not in Go so much)), it takes more work, so you're less inclined to do it. It remains unclear to me what "the best" is. Error-returning code is, both in theory and in my practice, more correct, in terms of the programming style it affords; it turns out that in ways you won't see if you've only ever done exceptions-based code that your exception handlers are often incorrect, overly generic, and missing opportunities to intelligently handle bugs. The flip side is that all that extra handling is of course expensive, and it's not 100% clear that it's worth the expense, and exception code, while IMHO clearly generally less correct in both theory and practice, is equally clearly often "good enough". I find myself wondering if some of the divisions of opinion come from different environments. In a conventional GUI program, I favor the exceptions; frankly so much stuff can go wrong that an enumeration of all possible failures is very difficult to produce, and your answer to almost all of them is likely to be identical ("scream an error to the user and hope they can fix it"). In a network server, I heavily favor the error returning paradigm; in my mature Go codebases that are network servers, I took a survey once and about 1/3rd of the lines that received an error immediately did something with them, other than simply passing them up the stack. Converting that to exceptions would actually lengthen the program and be a royal pain to deal with going forward.
- bluetomcat 11y ago> Much of the trouble with error returns comes from the strange C convention that functions with return values can be called as if they didn't return a value. The "warn_unused_result" GCC function attribute makes the compiler emit a warning when you do `func(something);`: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attribute...
- w0000t 11y agoThe problem is it needs to be applied to every function that returns a value. And I'm pretty sure you can't apply it to C library functions. Gcc seriously needs a global flag for this.
- jcranmer 11y agoYou might be surprised at the warnings you'd get. Did you know, for example, that printf returns a value? It's also a return value that rarely, if ever, tends to be useful.
- bluetomcat 11y agoAs a joke, in case you are really paranoic about error handling, you might end up with this :-) int written = printf("Got here"); if (written != sizeof("Got here") - 1) { written = fprintf(stderr, "Could not print my sacred message\n"); if (written != sizeof("Could not print my sacred message\n") - 1) { exit(I_DONT_KNOW_WTF_IS_HAPENNING_HERE); } }
- w0000t 11y agoThat sort of thing is actually a completely valid approach. You made a small mistake, since fprintf and printf call the same underlying code, the error handling should be either log the error in any way still available, and then depending on the design of the whole program, exit or continue. If printf fails you know exactly what is going on or have to exit. ( And printf would be wrapped in a function if you plan to use it repeatedly. )
- wereHamster 11y agoMost statically typed languages prefer 3 over 4. For the simple reason that it's easier for a compiler to verify that you've handled all the success and error cases. For Haskell though there exists a library which allows the compiler to track exceptions in much the same way as normal values, and thus warn you when you fail to handle an exception in a particular code path.
- chriswarbo 11y agoThe Haskell approach would be to use `Maybe` or `Either SomeError`, and have Functor/Applicative/Monad/etc. handle the error propagation. More experimental approaches are using Algebraic Effects, which are like resumable exceptions, tracked in the type system, with ambient handlers. It's an elegant way to solve this problem, mentioned in the article: > However returning error codes makes error propagation difficult. When there’s need to propagate the error up the stack several layers, every layer needs to make sure that they do it correctly... ideally most of the code along the way should be error agnostic and not really need to know about the details of the possible errors but still be able to propagate them up to the caller without a hitch.
- qznc 11y agoExceptions have another advantage not mentioned in the article: performance. If you implement them as "zero cost exceptions", then there is no overhead for the successful path. Option 3 requires error checking even if you return successful, which can only be optimized away maybe if the compiler inlines and moves code around. Since Go and Rust favor static linking this might work. If you want to link libraries it does not.
- qznc 11y agoI like to say "Rust has exceptions, you just can't catch them". If you use panic!() in Rust, it will unwind the stack, just like exceptions do. Destructors correspond to finally blocks in Java.
- sanderjd 11y agoAlso worth noting that "things come unglued" applies to panics in destructors in Rust the way it applies to exceptions in destructors in C++.
- bluetomcat 11y agoHe talks about "soft" errors as if the only way to handle them is to propagate them up to the UI layer, displaying a message. In many situations the error handling affects the code paths at a more profound level. Something might be considered an error in the callee, but at the same time it might be an acceptable result in the caller, for which a well-defined path exists. Throwing an exception in such situations is just disconnecting the faulting code from the error handling code.
- einrealist 11y agoThe perspective is about layered code and reuse. So what is wrong with delegation? My file access library should not know that it is linked in an application with an UI. Throwing an exception just delegates the issue to the code which depends on me. Whether the IO exception is really a problem can only be decided by the user of my library.
- nyir 11y agoAnd option number five, conditions + restarts, http://www.lispworks.com/documentation/lw61/CLHS/Body/09_a.htm http://www.lispworks.com/documentation/lw61/CLHS/Body/09_a.h..., which is a bit like more formalised callbacks / exceptions depending on usage. Restarts are a great thing for both interactive (using a "continue" restart if e.g. something can be safely skipped) and programmatic usage (to customise an algorithm where you control from the outside whether a restart should be invoked in case of a "soft" exception/condition).
- Const-me 11y agoExceptions only work well in managed languages like C#, Java, Python. In C++ they don’t. In C++, error codes FTW. On Windows you usually return HRESULT, call FAILED/SUCEEDED macros to check, call OS-provided FormatMessage API to format an error (the messages are of course localized). If you’re using some 3rd party library that uses its own error codes, it’s usually trivial to pack them in HRESULT when failed, check HRESULT facility when formatting. In C++, exceptions have fatal disadvantage: they don’t work across modules. Two reasons: (1) C++ exceptions aren’t binary compatible across compilers (2) memory management isn’t compatible across compilers either, if some module has called new/malloc, the same module must call delete/free
- stinos 11y agoDon't really agree, this seems too general? It's a matter of preference, what level you're working on and of what is possible. For example you're writing C++ which is going to be wrapped in a C-style api then yes you are going to need those error codes. If you are writing an application using some C++ api with well-defined exceptions then using them might lead to much nicer code. Consider some function in main() which takes care of a ton of initialization; I just happen to prefer try { MethodA(); MethodB(); MethodC(); } catch( const FooException& e ) { //show e return 1; } catch( const BarException& e ) { //show e return 1; } over auto resulta = MethodA(); if( FAILED( resulta ) ) { //show result return 1; } auto resultb = MethodB(); if( FAILED( resultb ) ) { //show result return 1; } auto resultc = MethodC(); if( FAILED( resultc ) ) { //show result return 1; } they don’t work across modules A problem which fades away if you build all code using the same compiler and build settings, which is a good idea anyway (there's not much which works across modules when mixing e.g. debug and release builds). I've written a lot of C++, and misbehaving across modules is like the one thing I didn't have problems with :]
- Const-me 11y ago1. Harder to maintain. Your colleague will change MethodB so it now throws an ExceptionB(), and your code will crash with unhandled exception = segfault. With error codes, your colleague may return any FAILED code she finds adequate. As long as you have a standard way of showing messages, the caller doesn’t need to change a single line. 2. You can greatly simplify your return codes example with a macro, e.g. #define CHECK( hr ) { HRESULT __hr = (hr); if( FAILED( __hr ) ) return __hr; } Then you will be able to handle them in the upper level. Also, by changing this macro to slightly more complex variant you can very easily log failed operations (remember that __FILE__, __LINE__, #hr, etc.) “if you build all code using the same compiler and build settings” — fine, what about using other people’s libraries you can’t or don’t want to compile? Like OS-provided API, CRT, middleware, etc? Also, what happens with your project code style consistency if you’ll decide to replace 3rd party component (that unlikely uses exceptions) with in-house, or vice versa? P.S. I’ve written a lot of C++ as well, I’ve been developing commercial software since 2000. My experience tells me if you’re going to achieve good quality of your product, it’s seldom sufficient to just print messages to stdout or MessageBox an exception message. Depending on what exactly happened and what the code was doing when something failed, it may be necessary to do something more complex that needs rich execution context. Like write stuff to logs, send error details to the client over a network in the response, close some windows, invoke a caller-provided error handler, etc. You don’t have that context where you catch() them.
- simula67 11y agoThis article caused me to lose my faith in exceptions : http://ptgmedia.pearsoncmg.com/images/020163371x/supplements/Exception_Handling_Article.html http://ptgmedia.pearsoncmg.com/images/020163371x/supplements...
- Quiark 11y agoThat article is extremely outdated and by today standards quite ill-informed. Many of its arguments could be equally made for error codes. Try some of these instead: http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Andrei-Alexandrescu-Systematic-Error-Handling-in-C www.boost.org/community/exception_safety.html https://github.com/Quiark/CppExceptDetails http://www.boost.org/community/error_handling.html
- asgard1024 11y agoRegarding what he calls "soft errors" (a better name would be environmental errors, I think), it's unfortunate that option 2 (error handlers) is so often neglected by programming language designers (for example Code Complete doesn't mention option 2 at all). In my view, just like option 4 (exceptions) is more general than option 3 (return values), also option 2 is more general than option 4. The biggest advantage of 2 compared to 4 is that recovery is much easier. However, more generality also unfortunately means more complexity (both for programmer and runtime). That's why many people here prefer 3 to 4 or 2, and neither one of them is really better than the other. Common Lisp is a good example of language that has good support for all three. It can return multiple values which facilitates option 3, it has usual exceptions as option 4, and most importantly, it also has signals and restarts that serve as option 2. I would classify what he calls hard errors into two categories. One is inconsistent input and the other internal (logic) error. This depends on level of view, if you are looking only at one module or a bigger whole (inconsistent input into one module from the other can be considered internal error in the whole). Now, I don't think inconsistent input should be dealt with asserts. If it's worth checking the inconsistent input at the module boundary, do it and return an error (in any of the three ways mentioned above). The caller should decide whether or not this is a logic error (it may also be bad input from the user), and how to handle the failure. While I agree with the statement that on logic errors one should generally fail hard, sometimes it's not desirable, for instance in server you may want to just restart the wrong thread instead of shutting down the whole server. Finally, I think asserts are very much underrated in the current development practices, compared to tests. I wish the effort spent on testing frameworks would be spent on frameworks that would let you add lots of asserts into the code and turn them on/off as needed (for example based on required tradeoff between reliability and performance).
- masklinn 11y agoIt's a bit sad (and an indictment to "modern" languages) that option 5 "Conditions" (resumable/non-unwinding exceptions) is not mentioned.
- qznc 11y agoHow is that different to option 2?
- masklinn 11y agoIt has nothing in common with 2. It's an extension of 4 where the eventual (dynamically scoped) handler is executed on the non-unwound stack and may unwind the stack (same as an exception), perform an action on the non-unwound system (e.g. resume, resume with a value provided, resume with a restart specified, repeat, etc…) or dynamically opt to resume looking up handlers.
- asgard1024 11y agoI have to disagree. Historically, conditions and restarts came from Multics and PL/I. There is also similar thing in IBM z/OS, descendant of MVS (ESTAE recovery routines). When ESTAE is invoked, you can decide where you want to recover (since it's assembler you may unwind stack if you have one), or you can "percolate", which means to pass processing on the next error handler. Unix signals is probably another descendant. So in the 1960s, recovery by error handler (the option 2) was very common thing. It only got a bit forgotten later due to them not being properly supported in C, so it didn't get into C++ and Java due to cultural disconnect.
- gorena 11y agoI find anything but a Result<T, E> ADT to be a poor solution. Errors shouldn't have language-level support (exceptions), and error codes are just a poor implementation of a result ADT - they cannot be enforced by the type system. Result is a monad, so it doesn't require an alternative code path. It's the cleanest and safest. "Exceptions" should always be for fatal, irrecoverable errors (array index out of bounds). I'm okay with having them, but I don't think languages should support "catch".
- cousin_it 11y agoHow about using an algebraic effect system? That's sort of a general purpose alternative to monads that also doesn't require an extra code path, and adds a bit of theoretical niceness. For example, if your code can throw two different exceptions, composing two monads is order-sensitive (because the monad interface is in some sense too general and forgets too much), while algebraic effects always commute with each other.
- gorena 11y agoTrue. I could rephrase as "the possibility of errors is just another type, and is best treated as such"?
- deleted 11y ago[deleted]
- ensiferum 11y agoSome interesting discussion here. First of all it seems that some people are still having a bit muddled vision about what is an exceptional error/condition. Of course you will need to first define what constitutes an exceptional situation in your application and its domain. The blog post used a file opening as an example. That example implied that in that software, in that particular context we have a requirement that our file must be opened. If for some reason it cannot be opened we have grounds for an exceptional condition. When we have this condition we then choose some particular method/means for how to indicate the situation and how to deal with it. We can also demonstrate this the otherway around. Someone mentioned as an example something like a (web)server. Obviously in that application's domain not being able to open and serve some file is just a part of the normal flow. Clients may make request to resources that do not exist. So in that example that would not be an exceptional circustamce but the "FILE NOT FOUND" condition is just part of the normal conditional flow of the program. However in that same server we might be for example reading the initial configuration file when the program is started and if we fail to open the file then we might have grounds for an exceptional situation. So to sum this up, what constitutes an exception situtation is all up to the domain of the program. The blog post discussed methods for dealing with that situation. This is an important point to realize. Finally a note on assert, yes there are cases when you can't hard fail and core dump on assert as much as you would like to. For example when implementing API's that define error return values for "hard failure" conditions. Whether this is sensible practice and makes the software world better or worse is a matter for another discussion.
- losvedir 11y agoNo discussion of errors and exceptions could be complete without including Erlang (/OTP)! How does its philosophy of fail fast and rely on your supervisor to restart fit into 1-4? I've been dabbling in Elixir lately, and I really like how easy it makes writing the "happy path", while still robustly handling errors.
- MishraAnurag 11y agoGreat post. Soft errors have a different meaning altogether in electronics. It's an interesting read. https://en.m.wikipedia.org/wiki/Soft_error https://en.m.wikipedia.org/wiki/Soft_error