Hacker News new | comments | show | ask | jobs | submit login
Error codes vs exceptions: critical code vs typical code (yosefk.com)
62 points by suraj on Sept 24, 2012 | hide | past | web | favorite | 26 comments

If you use C++ without exceptions, it means you cannot use the STL or any library that throws. Pretty weak.

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.

What a weird claim. Not only do you not need exceptions to use the C++ standard library, but you can (or at least used to be able to) compile C++ code without support for exceptions.

Also, exceptions are not just a fancy goto, at least in C++. They're a fancy longjmp. Goto doesn't unwind the stack.


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.

You actually need exceptions anywhere you do new, unless you exclusively use new(std::nothrow) (hint: you don’t). On top of that the STL may throw exceptions such as std::out_of_range as I responded in another comment. And again, you have to review all your libraries to make sure they don’t throw.

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...

Exceptions aren’t really like “goto”—they’re dynamically bound, dynamically typed, nonlocal “return” statements. Which incidentally is a useless mechanism for error handling unless you have some kind of destruction semantics, which is mentioned in the article. Anyway, given the choice between exceptions and return values for error handling, I go for return values that the compiler can force me to check.

STL "containers" and "streams" that need exceptions are pretty useless for any sort of critical code IMHO. STL "algorithms", on the other hand, are pretty useful and work with exceptions disabled just fine.

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.

If this is FUD, please downvote me into grayness, but I remember an article - or maybe even a book - proving that exception-safety in C++ was impossible. I remember nothing else about this; it was probably around 1999-2000.

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.

It is flatly untrue that you cannot use the C++ STL if you don't use exceptions. std::nothrow exists for exactly that reason.

I'm afraid you might be confusing a parameter supplied to the memory allocator (e.g. nothrow) and the throwing specifications of the STL.

Example: some containers throw std::out_of_range if you access a non-existing element and this has nothing to do with std::nothrow.

The only core thing I can think of is “std::bad_cast” with “dynamic_cast”, but I’ve never run into that in practice. “std::out_of_range” only happens when you’re using checked member functions that take indices—which I never do, preferring iterators. However, “std::length_error” can theoretically happen, and nobody checks for it, so there is at least one point of failure.

The libcxx stl uses a #define to for exception support or not. When exceptions are disabled, they assert() instead.

Can someone who uses exceptions in large systems offer a comment on which exceptions they throw?

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?

Given your scenario, you are indeed correct: throwing implementation-specific exceptions leaks internal details and further couples clients of the API. Can you imagine using such an API which forces you to continually handle new exceptions that arise from new implementations? As you suggest, it's better to create an exception for the API which then wraps the underlying exception. I would not go so far as to create a family or hierarchy of API exceptions unless there's a need to communicate each of them individually.

What usually happens in practice is a combination of none and all the above. A bit of a mess, really.

When I "simplify" a bunch of stuff into a nice pretty "thing", I'll catch all exceptions (Throwable) and then re-throw if. Depending on the thing, I'll stick an enum in my exception. Usually, I only care about "should I retry?" or "give up"

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

Example from the article criticizing exceptions:

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:

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-...

You can do the same thing in C++ RAII with an extra function scope though. Just create a Gate object or whatever and drop it on the stack before you call the wait_for... thing. An exception thrown out of that scope will clean up the Gate object for you and close it. Or just use Java, which has a "finally" syntax that does the same thing. Or Python's "with", which match D very closely indeed.

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.

No discussion of Exceptions vs Error codes is complete without referring to Joel Spolsky's Exceptions article:




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.

Java world -> Aren't checked exceptions just error codes that use types instead of arbitrary values and compiler gaurantees?

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.

This is why I hate all modern programming languages (I'm actually writing an article on that >.>). The conclusion to this article is the same as many others. Programming language feature X is better in situation A and Programming language feature Y is better in situation B. Where the features are often incompatible (or fulfill similar areas, error handling in this case). The solution is of course to have multiple programming languages in your projects that have both situation A and situation B

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.

Another weak article. He never mentions

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.

Another person who didn't read the whole article? ;) Near the end he mentions RAII in C++, with in Python, and using in C#.

It's very good that you understand how to write exception-safe code (although makeAMess() should typically be OUTSIDE the try {} block), but he's clearly comparing the NAIVE cases, i.e., people not paying attention to errors in either idiom.

Perhaps Mr Kreinin has C++ in mind? (Though this would be at odds with his idea that an exception includes a call stack, so maybe not.)

Exceptions are far far more readable than error code handling. Exceptions allow one to separate error handling from the structure of the "mainline" execution path of an algorithm. Weaving if/else statements in and out of the mainline path of an algorithm muddies the intent. Exceptions help with this greatly. Instead of having to comprehend the entirety of the algorithm (when exception handling can sometimes take up 50% or more of the code), you can zero in on the most common execution path. Then understand the degenerate cases in turn. The fewer branches your brain has to process in any given moment is a boon for readability.

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.

> 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.

You took my comparison with goto entirely too literal. The point was more about code organization rather than implementation details. This should have been clear from the context.

>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.

And C++ exceptions are the worst of both worlds (uncaught C++ exceptions have no call-stack; they are neither local, nor explicit).

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact