128

I've been pondering this problem for a while now and find myself continually finding caveats and contradictions, so I'm hoping someone can produce a conclusion to the following:

Favour exceptions over error codes

As far as I'm aware, from working in the industry for four years, reading books and blogs, etc. the current best practice for handling errors is to throw exceptions, rather than returning error codes (not necessarily an error code, but a type representing an error).

But - to me this seems to contradict...

Coding to interfaces, not implementations

We code to interfaces or abstractions to reduce coupling. We don't know, or want to know, the specific type and implementation of an interface. So how can we possibly know what exceptions we should be looking to catch? The implementation could throw 10 different exceptions, or it could throw none. When we catch an exception surely we're making assumptions about the implementation?

Unless - the interface has...

Exception specifications

Some languages allow developers to state that certain methods throw certain exceptions (Java for example, uses the throws keyword.) From the calling code's point of view this seems fine - we know explicitly which exceptions we might need to catch.

But - this seems to suggest a...

Leaky abstraction

Why should an interface specify which exceptions can be thrown? What if the implementation doesn't need to throw an exception, or needs to throw other exceptions? There's no way, at an interface level, to know which exceptions an implementation may want to throw.

So...

To conclude

Why are exceptions preferred when they seem (in my eyes) to contradict software best practices? And, if error codes are so bad (and I don't need to be sold on the vices of error codes), is there another alternative? What is the current (or soon to be) state of the art for error handling that meets the requirements of best practices as outlined above, but doesn't rely on calling code checking the return value of error codes?

RichK
  • 1,457
  • 2
  • 12
  • 12
  • 15
    I don't get your argument about leaky abstractions. Specifying what exception a particular method *may* throw is *part of the interface specification*. Just as the type of the return value is part of the interface specification. Exceptions just don't "come out of the left side" of the function, but they're still part of the interface. – deceze May 02 '12 at 12:23
  • 1
    @deceze How can an interface possibly state what an implementation may throw? It may throw all the exceptions in the type system! And that many languages don't support exception specifications suggest they're pretty dodgy – RichK May 02 '12 at 12:28
  • 1
    I'd agree that it can be tricky to manage all the different exceptions a method may throw, if it itself uses other methods and does not catch their exceptions internally. Having said that, it's not a contradiction. – deceze May 02 '12 at 12:36
  • 2
    The leaky assumption here is that code can *handle* an exception when it escapes the interface boundary. That's very unlikely, given that it doesn't know nearly enough about what actually went wrong. All it knows is that something went wrong and the interface implementation didn't know how to deal with it. The odds that it can do a better job are poor, beyond reporting it and terminating the program. Or ignoring it if the interface isn't critical to proper program operation. An implementation detail that you can't and shouldn't encode in an interface contract. – Hans Passant May 02 '12 at 13:17
  • The system is made of objects exchanging messages. There are no exceptions to that architecture. – DBJDBJ Feb 05 '21 at 08:09

11 Answers11

34

First of all, I would disagree with this statement:

Favour exceptions over error codes

This is not always the case: for example, take a look at Objective-C (with the Foundation framework). There the NSError is the preferred way to handle errors, despite the existence of what a Java developer would call true exceptions: @try, @catch, @throw, NSException class, etc.

However it is true that many interfaces leak their abstractions with the exceptions thrown. It is my belief that this is not the fault of the "exception"-style of error propagating/handling. In general I believe the best advice about error handling is this:

Deal with the error/exception at the lowest possible level, period

I think if one sticks to that rule of thumb, the amount of "leakage" from abstractions can be very limited and contained.

On whether exceptions thrown by a method should be part of its declaration, I believe they should: they are part of the contract defined by this interface: This method does A, or fails with B or C.

For example, if a class is an XML Parser, a part of its design should be to indicate that the XML file provided is just plain wrong. In Java, you normally do so by declaring the exceptions you expect to encounter and adding them to the throws part of the declaration of the method. On the other hand, if one of the parsing algorithms failed, there's no reason to pass that exception above unhandled.

It all boils down to one thing: Good interface design. If you design your interface well enough, no amount of exceptions should haunt you. Otherwise, it's not just exceptions that would bother you.

Also, I think the creators of Java had very strong security reasons to include exceptions to a method declaration/definition.

One last thing: Some languages, Eiffel for example, have other mechanisms for error handling and simply do not include throwing capabilities. There, an 'exception' of sort is automatically raised when a postcondition for a routine is not satisfied.

K.Steff
  • 4,475
  • 2
  • 31
  • 28
  • 14
    +1 for negating "Favour exceptions over error codes." I've been taught that exceptions are well and good, so long as they are, in fact, exceptions and not the rule. Calling a method which throws an exception to determine whether or not a condition is true is extremely bad practice. – Neil May 03 '12 at 12:59
  • See [Joel on Exceptions](http://www.joelonsoftware.com/items/2003/10/13.html) for another vote to negate the assumption. – Joshua Drake May 03 '12 at 16:33
  • 5
    @JoshuaDrake: He's definitely wrong. Exceptions and `goto` are very different. For example, exceptions always go in the same direction- down the call stack. And secondly, protecting yourself from surprise exceptions is exactly the same practice as DRY- for example, in C++, if you use RAII to ensure cleanup, then it ensures cleanup in *all* cases, not just exception but also all normal control flow. This is infinitely more reliable. `try/finally` accomplishes something somewhat similar. When you properly guarantee cleanup, you don't need to consider exceptions as a special case. – DeadMG May 03 '12 at 16:39
  • @DeadMG In C#, at least, that only holds when catching them. Throwing exceptions causes your code to exit at that point and go somewhere else. If you throw an unhandled exception then you immediately move up the stack and have no idea where the code will begin executing again. [Unhandled Exception Processing In The CLR](http://msdn.microsoft.com/en-us/magazine/cc793966.aspx#id0070028). But, also note that I only posted the article in favor of challenging the assumption that `Favour exceptions over error codes` was the only way to go, not to agree with Joel. – Joshua Drake May 03 '12 at 20:23
  • 1
    @JoshuaDrake: But you know where it will begin executing again- the highest-level appropriate `catch`. And it can only go one way- unlike `goto` which can jump arbitrarily. – DeadMG May 04 '12 at 01:00
  • 5
    @JoshuaDrake Sorry, but Joel is way off there. Exceptions are not the same as goto, you always go at least one level up the call stack. And what do you do if the level above can't immediately deal with the error code? Return yet another error code? Part of the problem with error does is that they CAN be ignored, leading to worse problems than the throwning of an exception. – Andy May 04 '12 at 01:56
  • 29
    -1 because I completely disagree with "Deal with the error/exception at the lowest possible level" - that's wrong, period. Deal with errors/exceptions at the *appropriate* level. Quite often that is a much higher level, and in that case, error codes are a huge pain. The reason to favor exceptions over error codes is that they allow you to choose freely at which level to deal with them without impacting the levels in between. – Michael Borgwardt May 04 '12 at 06:56
  • @DeadMG From the msdn article linked in my comment `The OS unhandled exception filter (UEF) mechanism may not always result in triggering the CLR's unhandled exception processing.` so I would say that you only think that you know. You argued that it went down the stack, Andy says it goes up the stack, so which is it? – Joshua Drake May 04 '12 at 11:49
  • @DeadMG: "But you know where it will begin executing again- the highest-level appropriate catch." What if an appropriate catch is missing because the exception is thrown from a class that you do not know of (because you are not supposed to know all the implementation details of the code you are calling)? – Giorgio May 07 '12 at 08:39
  • @Michael Borgwardt: This is possible if you can declare the exceptions thrown by each method. This is only possible in Java: C++, C#, Scala do not provide this facility, so it is difficult to know which exceptions to catch if you don't even know which exceptions are thrown and you have to look at the implementation to know this. – Giorgio May 07 '12 at 08:48
  • @Giorgio: usually, high-level exception handling is very generic and doesn't really care about what exception it is. Java's checked exception are a niche theoretical solution, but in practice they just lead to polluted APIs. There's a reason nobody else adopted them, not even other JVM languages. – Michael Borgwardt May 07 '12 at 08:58
  • @Michael Borgwardt: What kinds of problems are caused by checked exceptions? – Giorgio May 07 '12 at 09:09
  • 2
    @Giorgio: see http://www.artima.com/intv/handcuffs.html - especially page 2 and 3. – Michael Borgwardt May 07 '12 at 09:34
  • @Michael Borgwardt: Thanks for the link. I understand your idea of handling exceptions at the appropriate level but, again, how to know what exceptions to catch if I do not know what exception can be thrown? Or do you mean one should handle the documented exceptions of the callee in the caller, and have a generic catch-all handler at a higher level in the call stack for any additional exceptions that might come out of the callee? – Giorgio Jun 13 '13 at 17:28
  • 1
    @Giorgio: Generally speaking, if you don't know an exception can be thrown, you shouldn't be catching it. Exceptions are meant to be used when something is truly exceptional and may cause problems downstream. Some exceptions can be recovered from, but their primary purpose is different from that of an error code. The user should never see them, but a developer should see them quite often, as they indicate a preventable bug. Catching is definitely not designed to be something you can always trust to work. – Magus Mar 12 '14 at 23:25
29

I would just like to note that exceptions and error codes aren't the only way to deal with errors and alternate code paths.

Out of the top of the mind, you can have an approach like the one taken by Haskell, where errors can be signaled via abstract data types with multiple constructors (think discriminated enums, or null pointers, but typesafe and with the possibility of adding syntatic sugar or helper functions to make the code flow look good).

func x = do
    a <- operationThatMightFail 10
    b <- operationThatMightFail 20
    c <- operationThatMightFail 30
    return (a + b + c)

operationThatMightfail is a function that returns a value wrapped in a Maybe. It works like a nullable pointer, but the do notation guarantees that the whole thing evaluates to null if any of a, b, or c fail. (and the compiler protects you from making an accidental NullPointerException)

Another possibility is passing an error handler object as an extra argument to every function you call. This error handler has a method for each possible "exception" that can be signaled by the function you pass it to, and can be used by that function to treat the exceptions where they occur, without necessarily having to rewind the stack via exceptions.

Common LISP does this, and makes it feasible by having syntatic support (implicit arguments) and having the built-in functions follow this protocol.

hugomg
  • 2,102
  • 13
  • 17
  • 1
    That's really neat. Thanks for answering the part about alternatives :) – RichK May 03 '12 at 16:34
  • 2
    BTW, exceptions is one of the most functional parts of modern imperative languages. Exceptions form a monad. Exceptions use patter matching. Exceptions help imitate applicative style without actually learning what `Maybe` is. – 9000 May 03 '12 at 17:28
  • I was reading a book about SML recently. It mentioned option types, exceptions (and continuations). The advice was to use option types when the **undefined** case is expected to occur rather often, and to use exceptions when the undefined case occurs very seldom. – Giorgio Jun 13 '13 at 17:33
10

Yes, exceptions can cause leaky abstractions. But are error codes not even worse in this regard?

One way to deal with this problem is to have the interface specify exactly which exceptions can be thrown under what circumstances and declare that implementations must map their internal exception model to this specification, by catching, converting and re-throwing exceptions if necessary. If you want a "prefect" interface, that's the way to go.

In practice, it's usually sufficient to specify exceptions that are logically part of the interface and which a client may want to catch and do something about. It's generally understood that there can be other exceptions when low-level errors happen or a bug manifests, and which a client can only handle generally by showing an error message and/or shutting down the application. At least the exception can still contain information that helps diagnosing the problem.

In fact, with error codes, pretty much the same thing ends up happening, just in a more implicit fashion, and with far more likelihood of information getting lost and the app ending up in an inconsistent state.

Michael Borgwardt
  • 51,037
  • 13
  • 124
  • 176
  • 1
    I do not understand why an error code can cause a leaky abstraction. If an error code is returned instead of a normal return value, and this behaviour is described in the specification of the function / method, then IMO there is no leak. Or am I overlooking something? – Giorgio May 04 '12 at 21:52
  • If the error code is specific to the implementation while the API is supposed to be implementation-agnostic, then specifying and returning the error code leaks unwanted implementation details. Typical example: a logging API with a file-based and a DB-based implementation, where the former may have a "disk full" error, and the latter a "DB connection refused by host" error. – Michael Borgwardt May 04 '12 at 22:04
  • I understand what you mean. If you want to report implementation specific errors then the API cannot be implementation agnostic (neither with error codes nor with exceptions). I guess the only solution would be to define an implementation-agnostic error code like "resource not available", or to decide that the API is not implementation-agnostic. – Giorgio May 04 '12 at 22:31
  • 2
    @Giorgio: Yes, reporting implementation-specific errors is tricky with an implementation-agnostic API. Still, you can do it with exceptions, because (unlike error codes) they can have multiple fields. So you can use the type of the exception to provide the generic error information (ResourceMissingException), and include an implementation-specific errorn code / message as a field. Best of both worlds :-). – sleske May 07 '12 at 08:13
  • And BTW, that is just what java.lang.SQLException does. It has `getSQLState` (generic) and `getErrorCode` (vendor-specific). Now if only it had proper subclasses... – sleske May 07 '12 at 08:15
  • @sleske: Also an object that is returned as a result can have multiple fields that you can query for error codes. – Giorgio Jun 13 '13 at 17:35
6

Lots of good stuff here, I'd just like to add that we should all be wary of code which uses exceptions as part of normal control flow. Sometimes people get into that trap where anything which isn't the usual case becomes an exception. I've even seen an exception used as a loop termination condition.

Exceptions mean "something I can't handle here happened, need to go out to someone else to figure out what to do." A user typing invalid input isn't an exception (that should be handled locally by the input by asking again, etc.).

Another degenerate case of exception usage I've seen is people whose first response is "throw an exception." This is almost always done without writing the catch (rule of thumb: write the catch first, then the throw statement). In big applications this becomes problematic when an uncaught exception bubbles up from the nether-regions and blows up the program.

I'm not anti-exceptions, but they seem like singletons of a few years ago: used far too frequently and inappropriately. They're perfect for the intended use, but that case is not as broad as some think.

anon
  • 1,474
  • 8
  • 8
5

Leaky abstraction

Why should an interface specify which exceptions can be thrown? What if the implementation doesn't need to throw an exception, or needs to throw other exceptions? There's no way, at an interface level, to know which exceptions an implementation may want to throw.

Nope. Exception specifications are in the same bucket as return and argument types- they are part of the interface. If you can't conform to that specification, then don't implement the interface. If you never throw, then that's fine. There's nothing leaky about specifying exceptions in an interface.

Error codes are beyond bad. They're terrible. You have to manually remember to check and propagate them, every time, for every call. This violates DRY, for a start, and massively blows up your error-handling code. This repetition is a far bigger problem than any faced by exceptions. You can never silently ignore an exception, but people can and do silently ignore return codes- definitely a bad thing.

DeadMG
  • 36,794
  • 8
  • 70
  • 139
  • Error codes can be easier to use if you have good forms of syntactic sugar or helper methods though, and in some languages you can make the compiler and type-system guarantee that you will never forget to handle an error code. As for the exception interface part, I think he was thinking about the notorious clunkyness of Java's checked exceptions. While they seem like a perfectly reasonable idea at first glance, they cause many painfull little problems in practice. – hugomg May 03 '12 at 16:55
  • @missingno: That's because, as per usual, Java has a terrible implementation of them, not because checked exceptions are inherently bad. – DeadMG May 03 '12 at 16:59
  • 1
    @missingno: What kinds of problems can be caused by checked exceptions? – Giorgio May 07 '12 at 09:11
  • 1
    @Giorgio: Its very hard to foresee every kind of exception that a method might throw, since this needs to take into account other subclasses and code that has yet to be written. In practice people end up with ugly workarounds that throw away information, like reusing a system exceptions class for everything or having to catch and re-throw inner exceptions frequently. I have also heard that checked exceptions were a big hurdle when they were trying to add anonymous functions to the language. – hugomg May 07 '12 at 15:01
  • @missingno: AFAIK anonymous functions in Java are just syntactic sugar for anonymous inner classes with exactly one method, so I am not sure I understand why checked exceptions would be a problem (but I admit I do not know much about the topic). Yes, it's hard to foresee what exceptions a method will throw, that's why IMO it can be useful to have checked exceptions, so that you do not have to guess. Of course you can write poor code to handle them, but this you can also do with unchecked exceptions. However, I know that the debate is quite complex and honestly I see pros & cons in both sides. – Giorgio May 07 '12 at 17:36
  • 1
    @Giorgio: Checked exceptions don't interact well with the generic system, making it harder to define many sorts of useful higher-order functions. http://james-iry.blogspot.com.br/2012/02/checked-exceptions-might-have-their.html – hugomg May 07 '12 at 20:04
3

Favour exceptions over error codes

  • Both should coexist.

  • Return error code when you anticipate certain behaviour.

  • Return exception when you did not anticipate some behaviour.

  • Error codes are normally associated with a single message, when exception type remains, but a message may vary

  • Exception has a stack trace, when error code doesn't. I don't use error codes to debug broken system.

Coding to interfaces not implementations

This might be specific to JAVA, but when I declare my interfaces I do not specify what exceptions might be thrown by an implementation of that interface, it just doesn't make sense.

When we catch an exception surely we're making assumptions about the implementation?

This is entirely up to you. You may try and catch a very specific type of exception and then catch a more general Exception. Why not let exception propagate up the stack and then handle it? Alternatively you can look at aspect programming where exception handling becomes a "pluggable" aspect.

What if the implementation doesn't need to throw an exception, or needs to throw other exceptions?

I don't understand why is it a problem for you. Yes, you may have one implementation that never fails or throws exceptions and you may have a different implementation that constantly fails and throws exception. If that's the case, then don't specify any exceptions on the interface and your problem is solved.

Would it change anything if instead of exception your implementation returned a result object? This object would contain outcome of your action along with any errors/failures if any. You can then interrogate that object.

CodeART
  • 3,992
  • 1
  • 20
  • 23
2

Well Exception handling can have their own interface implementation. Depending on the the type of the exception thrown, perform the desired steps.

The solution to your design problem is to have two interface/abstraction implementations. One for the functionality and the other for exception handling. And depending on the type of the Exception caught, call appropriate exception type class.

Implementation of Error codes is an orthodox way of handling exception. It is like usage of string vs. string builder.

Estefany Velez
  • 389
  • 2
  • 9
2

Leaky abstraction

Why should an interface specify which exceptions can be thrown? What if the implementation doesn't need to throw an exception, or needs to throw other exceptions? There's no way, at an interface level, to know which exceptions an implementation may want to throw.

In my experience, the code that receives the error (be it via exception, error code or anything else) would not normally care for the exact cause of the error - it would react the same way to any failure, except for possible reporting of the error (be it an error dialog or some kind of log); and this reporting would be done orthogonally to the code that called the failing procedure. For example, this code could pass the error to some other piece of code that knows how to report specific errors (e.g. format a message string), possibly attaching some context information.

Of course, in some cases it is needed to attach specific semantics to errors and react differently based on which error occured. Such cases should be documented in the interface specification. However, the interface may still reserve the right to throw other exceptions with no specific meaning.

Ambroz Bizjak
  • 521
  • 4
  • 7
1

IM-very-HO exceptions are to be judged on a case by case basis, because by breaking control flow they will increase the actual and perceived complexity of your code, in many cases unnecessarily so. Putting aside the discussion related to throwing exceptions inside your functions - which may actually improve your control flow, if one is to look at throwing exceptions through call boundaries, consider the following:

Allowing a callee to break your control flow may not provide any real benefit, and there may be no meaningful way to deal with the exception. For a direct example, if one is implementing the Observable pattern (in a language such as C# where you have events everywhere and no explicit throws in the definition), there is no actual reason to let the Observer break your control flow if it crashes, and no meaningful way to deal with their stuff (of course, a good neighbor should not throw when observing, but nobody's perfect).

The observation above can be extended to any loosely coupled interface (as you pointed out); I think it's actually a norm that after creeping up 3-6 stack frames, an uncaught exception is likely to end up in a section of code which either:

  • is too abstract to deal with the exception in any meaningful way, even if the exception itself is upcasted;
  • is performing a generic function (couldn't care less about why you failed, such as a message pump, or the observable);
  • is specific, but with a different responsibility, and it really shouldn't worry;

Considering the above, decorating interfaces with throws semantics is only a marginal functional gain, because a lot of callers thru interface contracts would only care if you failed, not why.

I would say it then becomes a question of taste and convenience: your primary focus is gracefully recovering your state in both the caller and the callee after an "exception", therefore, if you have a lot of experience in moving error codes around (coming from a C background), or if you're working in an environment where exceptions can turn evil (C++), I don't believe that throwing stuff around is so important for nice, clean OOP that you can't rely on your old patterns if you're uncomfortable with it. Especially if it leads to breaking SoC.

From a theoretical perspective, I think a SoC-kosher way of handling exceptions can be derived directly from the observation that most of the times the direct caller only cares that you failed, not why. The callee throws, someone very close above (2-3 frames) catches an upcasted version, and the actual exception is always sunk to a specialized error handler (even if only tracing) - this is where AOP would come in handy, because these handlers are likely to be horizontal.

vski
  • 1,064
  • 5
  • 10
1

I find that exceptions allow to write a more structured and concise code for reporting and handling errors: using error codes requires checking return values after every call and deciding what to do in case of an unexpected result.

On the other hand I agree that exceptions reveal implementation details that should be hidden to the code invoking an interface. Since it is not possible to know a priori which piece of code can throw which exceptions (unless they are declared in the method signature like in Java), by using exceptions we are introducing very complex implicit dependencies between different parts of the code, which is against the principle of minimizing dependencies.

Summarizing:

  • I think exceptions allow a cleaner code and a more aggressive approach to testing and debugging because uncaught exceptions are much more visible and difficult to ignore than error codes (fail soon).
  • On the other hand, uncaught exception bugs that are not discovered during testing can appear in a production environment in the form of a crash. In certain circumstances this behaviour is not acceptable and in this case I think using error codes is a more robust approach.
Giorgio
  • 19,486
  • 16
  • 84
  • 135
  • 2
    I disagree. Yes, uncaught exceptions can crash an application, but unchecked error codes can as well - so it's a wash. If you use exceptions properly, the only uncaught ones will be the fatal ones (like OutOfMemory), and for these, crashing right away is the best you can do. – sleske May 07 '12 at 08:19
  • The error code is part of the contract between the caller m1 and the callee m2: the possible error codes are defined in the interface of m2 only. With exceptions (unless you are using Java and declaring all thrown exceptions in method signatures) you have an implicit contract between the caller m1 and all methods that can be called by m2, recursively. So of course it is a mistake not to check a returned error code, but it is always possible to do it. On the other hand, it is not always possible to check all exceptions thrown by a method, unless you know how it is implemented. – Giorgio May 07 '12 at 08:25
  • First: You can check all exceptions - just do "Pokemon exception handling" (gotta catch them all - i.e. `catch Exception` or even `Throwable`, or equivalent). – sleske May 07 '12 at 08:46
  • 1
    In practice, if the API is properly designed, it will specify all exceptions that the client can handle meaningfully - these need to be caught specifically. These are the equivalents of error codes). Any other exception means "internal error", and the application will need to shut down, or at least shut down the corresponding subsystem. You can catch these if you want to (see above), but you usually should just let them bubble up. That "bubbling up" is the main advantage of using exceptions. You can still catch them farther up, or not, depending on needs. – sleske May 07 '12 at 08:48
-1

Right-biased Either.

It can't be ignored, it must be handled, it's completely transparent. And IF you use the correct left handed error type it conveys all the same information as a java exception.

Downside? Code with proper error handling looks disgusting (true of all mechanisms).

Keynan
  • 99
  • this doesn't seem to offer anything substantial over points made and explained in prior 10 answers. Why bumping two years old question with stuff like that – gnat Dec 22 '14 at 13:53
  • Except no one here mentioned the right biased either. hugomg got close talking about haskell, however Maybe is a shit error handler as it leaves no explanation of why the error occurred, nor any direct method with which to recover, and call-backs are one of the greatest sins in control flow design. And this question came up on google. – Keynan Dec 23 '14 at 23:05