Error codes vs exceptions: critical code vs typical code
yosefk.com
yosefk.com
Additionally, almost all examples of "exceptions are bad", fall short because of RAII (at least in C++) or finally clauses (in other languages).
Good reasons for (almost) not using exceptions (in C++):
- Performances
- "Expectability"/readability
- Platform-dependant constraints
There are probably others, but the article only talks about the "readability" arguments without even mentioning things such as exceptions specifications.
One thing you need to remember about exceptions: they're nothing more than a fancy goto.
Example: some containers throw std::out_of_range if you access a non-existing element and this has nothing to do with std::nothrow.
Also, exceptions are not just a fancy goto, at least in C++. They're a fancy longjmp. Goto doesn't unwind the stack.
Edit
Wait wait wait it appears I am wrong about this; the STL we used could be configured not to use exceptions but wow, that sucks, you need them for the standard library.
Exceptions are a fancy goto, longjmp is more accurate if you will, but it is my understanding that longjmp will not call all the appropriate destructors, it will just restore the environment which can lead to resources and memory leaks, from the standard:
If any automatic objects would be destroyed by a thrown exception transferring control to another (destination) point in the program, then a call to longjmp(jbuf, val) at the throw point that transfers control to the same (destination) point has undefined behavior.
Some compilers do add the appropriate clean up code to make longjmp cooperative to that aspect, so I might be just nitpicking a little bit here.
What I meant by "fancy goto" is that basically exceptions break the flow and can make the code much, much harder to review and making, solid and reliable code with exceptions is much, much harder than you might think (in C++), Raymond explained it better than me: http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/35294...
This, of course, depends on your definition of "critical code". I, for one, don't see how anything critical can rely on the single memory allocator that came bundled with the compiler.
Am I completely making that up? Or did we discover ways around the "impossible" part? I haven't coded in C++ since, uh, gcc 2.97 or so.
One of the problems I have in trying to design with exceptions is that they seem to offer a lot of potential to break abstraction layers.
As a concrete example, if you have a cacheing layer which - say - is implemented over the filesystem. In the event of an inability to access the correct location, the low-level routines might throw a permission-related exception. If you swap out the filesystem cacheing implementation with, say, one based on memcached you might instead get a network-related exception.
Neither of these types really make sense in the context of a 'cacheing layer error', they seem to leak implementation details - breaking the abstraction.
Do people accept this, or do they catch-and-rethrow new exception types at major layer boundaries? (e.g. in the above two implementations, both low-level exceptions may be caught and rethrown as "CacheInitialisationError" with some perhaps some additional diagnostic about the underlying cause). If they have such API-specific error types, do you go to the trouble of modelling an exception type hierarchy? So all your 'cache layer' exceptions inherit from 'CacheError' so a higher level can catch all "CacheExceptions" in one place? This seems like a lot of additional modelling effort, is it commonplace?
To my mind, the possible exceptions a API call may make (or equivalently, the errors it could return) are part of the API, and the errors need to be at the same conceptual level as the rest of the API. (So you can't have filesystem errors in a generic cache API).
What happens in practice?
What usually happens in practice is a combination of none and all the above. A bit of a mess, really.
Personally, I think exceptions were a mistake for errors. I prefer how you do error codes via ocaml.
let val = func ... in match val with Result(v) -> do nice thing | Error(500) -> retry | Error(503) -> retry | Error(_) -> abort
open_the_gate()
wait_for_our_men_to_come_in()
close_the_gate()
D programming language for example has an excellent support for RAII with ScopeGuard statement http://dlang.org/statement.html#ScopeGuardStatement The example in question will become: open_the_gate();
scope(exit)
close_the_gate();
wait_for_our_men_to_come_in()
Then even if wait_for_our_men_to_come_in() throws we still close the gate. There is additional granularity - we can execute statement on success or failure too.The technique was originally developed by Andrei Alexandrescu for C++ and described in http://www.drdobbs.com/cpp/generic-change-the-way-you-write-...
The point is that D is hardly unique about this. And further: the fact that D has syntax that makes this operation clean doesn't mean that it will be used. That first example can be written in any language, and often is. The "open/close" pair is clear and obvious in this example for didactic purposes, but in old code under maintenance that kind of assumption can be very subtle.
Syntax won't save you here. The problem being explained (which I only partially agree with) is that the whole exception model is flawed.
http://www.joelonsoftware.com/items/2003/10/13.html
Also:
http://channel9.msdn.com/Blogs/Charles/Erik-Meijer-and-Rober...
Erik Meijer interviews Robert Griesemer about Go. They chat about exceptions for error codes. The exception talk starts ~8:00.
Erik is in favor of failing fast with exceptions b/c most of the time, when you have an exception in an application, you want to fail fast because there is "nothing you can do" at the point where you have an unexpected null, etc.
Very interesting discussion between two smart guys.
And wasn't the thought that checked exceptions would be better for critical code because the compiler guaranteed that they were handled.
I'm no fan of java's error handling, but I think that the insinuation that the lack of any formal error handling makes you write less error prone code is bordering on the absurd.
Detail oriented individuals write robust code given the constraints of their environment. If its arbitrary strings that indicate error state they use strings if its integer return codes they use integer return codes.
But languages are so annoying to integrate (where they can call into each other and support each other's features, without ridiculous glue code. Let alone swap code in the middle of a file), unless they were designed to (e.g. python and C). It is hell to set up the library dependencies and get the compilers to cooperate with each other. Which is why most just pick the language that's best for the majority of their project (or spend large amounts of developer time integrating other programming languages (not scripting languages, that's related but not quite the point) into their project).
Which is why we need the ability to change semantic languages within the same programming system, and have the programming system resolve the differences between the semantic languages automatically. So you can have one semantic language with features designed for situation A and another with features designed for situation B and just swap between them as needed and have them compile together automatically. This is what my Masters thesis and probably what my PhD dissertation will be on.
try { makeAMess() errorProneOperation() } finally { cleanUp() }
your first instinct should be to use finally, not catch, but Java brought us the tragedy of checked exceptions (in which your #1 motivation in working with exceptions is to shut up the compiler) and the bad habits have been adopted by people who use other languages that copy the (otherwise pretty good) Java exception handling style.
Yes, exceptions are just fancy goto's--but that is a good thing! Goto's when used sparingly can greatly increase the readability of exception handling code. Exceptions simply create a language level abstraction for this functionality.
That is entirely incorrect, except in the odd case of Sinclair ZX Spectrum Basic, which is the only programming language that I"m aware of that allowed you to do "LET a=10: GOTO a" (all other languages with goto require a label; GCC has an extension for "indirect goto" and "label address" that is somewhat similar, although it restricts you to staying within the same function scope).
If anything, it's a fancy "longjmp", that also calls destructors along the way.
The big difference, and it is HUGE, is that when you do a "goto", you know where you the next instruction is coming from. When you throw an exception, you do not know which instruction will execute next from looking at the throw site (and in fact, you may need to conceptually inspect an unbounded number of stack frames to tell; which is different from longjmp or expression goto which only require you to examine one memory location).
> Exceptions allow one to separate error handling from the structure of the "mainline" execution path of an algorithm
In theory. In practice, they essentially guarantee that the error handling is not properly tested, and often useless except in the case of "everyday expected errors", which are not really exceptional, but rather quite frequent.
>In practice, they essentially guarantee that the error handling is not properly tested, and often useless except in the case of "everyday expected errors", which are not really exceptional, but rather quite frequent.
This hasn't been my experience.