Errors and Exceptions
giantfublog.wordpress.com
giantfublog.wordpress.com
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.
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.
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.
"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.
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.
My understanding exceptions only bubble up if they weren't handled at the point of failure. This is exactly the same if you don't check the return type of a function that could return error. Both of this situations point to poor programming technique rather than underlying implementation option.
One thing that makes me currently prefer exceptions to error returns is that within a try/with block you can write the code as you'd like to happen and hence easier to understand and maintain. All exception handling can happen within the respective catch blocks.
And if you say "why would you let exceptions bubble up that much", well, that is the whole point of exceptions, that they bubble up. If you say "to get rid of nonlocality just catch outside every call", well, now that's equivalent to checking return values always, but more error-prone.
This would allow you to check stuff on places you desire (sometimes right after function, sometimes in the UI thread) and have tooling support for managing what is left unchecked.
Let's build something like that for C# using Roslyn! Issues of Java-style checked exceptions can be overcome using proper typing with generics.
Let's say we had the following C++ code:
int foo(int (*bar)(char), int (*baz)(), char c) {
try {
return bar(c)
} catch(SomeException &e) {
raise otherexception;
}
}
What exceptions can be raised by foo?The answer is simply computed - "anything raised by its first argument except SomeException, plus the type of otherexception".
Such a system would be tremendously more flexible than Java's checked exceptions, while still allowing you to confidently restrict what might be thrown in a section of code.
var f = (n) => n / 5; f = (n) => 5 / n;
What I hope for is that usual patterns would survive in a convenient manner. For example, calling function with function argument that is evaluated inside this function is something that could be handled by language without too many problems.
Exception handling must be trained and educated. There is no silver bullet.
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.
A server connected to a network socket is very close to the wire protocol, and the potential error states are fairly well known and documented. Head up the abstraction stack deep into business logic and add a dose of third-party libraries and some high-level application structure (i.e. indirection and abstraction boundaries) and the world gets a lot more fuzzy.
I concur re GUI; I think this comes from the complexity of the GUI framework - "don't call us, we'll call you" - GUI code is typically a thin seam between two sets of framework call stacks.
I wouldn't advocate exceptions for a kernel, or a database server. The more code between you and the hardware, and the more code that you use that you don't control, the more I feel the need for exceptions to communicate error information. The higher up the stack you go, it's more likely that human intervention will be required to solve the problem - in a server situation, log the error and tell the user that support has been informed about the problem. Etc.
Higher up the stack, it's more likely you're dealing with an abstract object of some kind with no direct correspondence with the outside world. Exceptions are more likely to be an unexpected state, where unexpected means an unexplored area of the state space owing to an increased number of moving parts.
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-Attribute...
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);
}
}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. )
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.
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.
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.
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.
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
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 modulesA 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 :]
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.
2. Indeed you can
what about using other people’s libraries you can’t or don’t want to compile? I do want to compile them, or if I can't then either it is not a problem (at least I never had problems with OS provided APIs and CRTs which would give such problems because they are designed to circumvent it) or I might not want to use the libraries (never occurred to me, maybe I'm lacking experience or the particular applications I wrote just never gave me the chance of having to be in such situation).
Also, what happens with your project code style not really an argument, same can be said for switching to a library which does use exceptions
regarding PS: most of my/our applications have so much logging the lack of execution context is usually not a problem. In addition to that in those rare cases where things go completely berserk on Windows we'd use full minidumps. Not sure if that is what you mean with execution context though as I don't see how logging/sending error details/other things you mention would be handled differently when using exceptions vs error codes.
Writing exception-safe code in C++ is very hard.
Depending on another module's authors to get their C++ exception handling right is the road to madness.
In the example above, the FAILED clauses clearly handle all the failures. In the exception handling example it is not clear if all the failures are caught, or if they are more that are meant to be caught at higher levels, or perhaps exceptions were forgotten. It becomes very difficult to reason about control flow because exceptions distribute it across the program and over time. Someone might add another exception: Oops, you didn't handle FileNotFoundException and your program bombs. You add a new exception that seems reasonable, but you wind up changing a hundred places in your source code (... and your callers! hope you have good customer support).
Exceptions are exceptional, they should not be used to return indications of failure. Exceptions should be used when it's lights-out important that something Really Bad be handled or the program will be killed. Most C++ systems I've worked with catch an exception near the top, do some diagnostics, and either tell the use "wups" or attempt to restart.
A "FileNotFound" exception from an open() function is just colossally stupid. I don't have a nicer way of saying that. If I saw code with this pattern in a project that I was on, I would remove it and make sure it didn't happen again.
Yes it is, and my example wasn't about such functionality anyway
The same Herb who said Prefer to Use Exceptions to Report Errors? (which makes no sense btw, just like saying to never ever use them)
Depending on another module's authors to get their C++ exception handling right is the road to madness.
Wait, so we should just forget about STL/boost/.. and reinvent all wheels? Or maybe just all code - depending on another module's authors to get their error code handling right, as in not ignoring all of them, is also madness.
You add a new exception that seems reasonable, but you wind up changing a hundred places in your source code (... and your callers! hope you have good customer support).
That is just not how proper code using exceptions gets written, ever. Anyone can make up similar horror stories for error codes (you are checking if HR is FILE_NOT_FOUND or FILE_NOT_ACCESSIBLE but suddenly the author changed the name into FOOFILE_NOT_FOUND and now you wind up changing a hundred places in your source code) Callers know what exceptions they can handle (if they don't the code is crap anyway) and handle those. If a caller wants to provide a strong guarantee the caller catches everything and nothing is forgotten. Everything else is truly exceptional and bubbles up, eventually to the top like all most C++ systems you saw.
Not weighing in on the broader discussion (in this comment), but more languages support exhaustiveness checking of case statements than of exceptions.
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.htmlIn 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).
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.
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".