11

There are many requirements needed for a system to properly convey and handle exceptions. There are also many options for a language to choose from to implement the concept.

Requirements for exceptions (in no particular order):

  1. Documentation: A language should have a mean to document exceptions an API can throw. Ideally this documentation medium should be machine usable to allow compilers and IDEs to provide support to the programmer.

  2. Transmit Exceptional Situations: This one is obvious, to allow a function to convey situations that prevent the called functionality from performing the expected action. In my opinion there are three big categories of such situations :

    2.1 Bugs in the code that cause some data to be invalid.

    2.2 Problems in configuration or other external resources.

    2.3 Resources that are inherently unreliable (network, file systems, databases, end-users etc). These are a bit of a corner case since their unreliable nature should have us expect their sporadic failures. In this case are these situations to be considered exceptional ?

  3. Provide enough information for code to handle it: The exceptions should provide sufficient information to the callee so that it can react and possibly handle the situation. the information should also be sufficient so that when logged this exceptions would provide enough context to a programmer to identify and isolate the offending statements and provide a solution.

  4. Provide confidence to the programmer about the current status of his code's execution state: The exception handling capabilities of a software system should be present enough to provide the needed safeguards while staying out of the way of the programmer so he can stay focused on the task at hand.

To cover these the following methods were implemented in various languages:

  1. Checked Exceptions Provide a great way to document exceptions, and theoretically when implemented correctly should provide ample reassurance that all is good. However the cost is such that many feel it more productive to simply bypass either by swallowing exceptions or re-throw them as unchecked exceptions. When used inappropriately checked exceptions pretty much looses all it's usefulness. Also, checked exceptions make it difficult to create a API that is stable in time. Implementations of a generic system within a specific domain will bring it's load of exceptional situation that would become hard to maintain using solely checked exceptions.

  2. Unchecked Exceptions - much more versatile than checked exception they fail to properly document the possible exceptional situations of a given implementation. They rely on ad-hoc documentation if at all. This creates situations where the unreliable nature of a medium is masked by an API that gives the appearance of reliability. Also when thrown these exceptions loose their meaning as they move back up through the abstraction layers. Since they are poorly documented a programmer cannot target them specifically and often needs to cast a much wider net than necessary to ensure that secondary systems, should they fail, do not bring down the whole system. Which brings us right back to the swallowing problem checked exceptions provided.

  3. Multistate return types Here it is to rely on a disjoint set, tuple, or other similar concept to return either the expected result or an object representing the exception. Here no stack unwinding, no cutting through code, everything executes normally but the return value must be validated for error prior to continuing. I have not really worked with this yet so cannot comment from experience I acknowledge it resolves some problems exceptions bypassing the normal flow but still it will suffer from much the same problems as the checked exceptions as being tiresome and constantly "in your face".

So the question is :

What is your experience in this matter and what, according to you is the best candidate to make a good exception handling system for a language to have ?


EDIT: Few minutes after writing this question I came across this post, spooky !

2
  • 2
    "it will suffer from much the same problems as the checked exceptions as being tiresome and constantly in your face": Not really: with proper language support you only have to program the "success path", with the underlying language machinery taking care of propagating errors.
    – Giorgio
    Jun 28, 2014 at 6:54
  • "A language should have a mean to document exceptions an API can throw. " - weeeel. In C++ "we" learned that this doesn't really work. All you can really usefully do is to state whether an API can throw any exception. (That really cutting a long story short, but I think looking at the noexcept story in C++ can yield very good insights for EH in C# and Java as well.)
    – Martin Ba
    Jul 27, 2016 at 21:48

5 Answers 5

11

In the early days of C++ we discovered that without some sort of generic programming, strongly typed languages were extremely unwieldy. We also discovered that checked exceptions and generic programming didn't work well together, and checked exceptions were essentially abandoned.

Multiset return types are great, but no replacement for exceptions. Without exceptions the code is full of error-checking noise.

The other problem with checked exceptions is that a change in the exceptions thrown by a low-level function forces a cascade of changes in all the callers, and their callers, and so on. The only way to prevent this is for each level of code to catch any exceptions thrown by lower levels and wrap them in a new exception. Again, you end up with very noisy code.

10
  • 2
    Generics do help solving a whole class of errors that are mostly due to a limitation of the language's support to OO paradigm. still though, the alternatives seem to be to either have code that mostly do error checking or that runs hoping nothings ever goes wrong. Either you have exceptional situations constantly in your face or live in a dream land of fluffy white bunnies that turn real ugly when you drop a big bad wolf in the middle !
    – Newtopian
    May 30, 2011 at 17:04
  • 3
    +1 for the cascading issue. Any system/architecture that makes change difficult only leads to monkey-patching and messy systems, no matter how well-designed the authors thought they were. May 30, 2011 at 17:08
  • 2
    @Newtopian: Templates do things that can't be done in strict object orientation, such as provide static type safety for generic containers. May 31, 2011 at 18:26
  • 2
    I would like to see an exception system with a concept of "checked exceptions", but one very different from Java's. Checked-ness should not be an attribute of an exception type, but rather throw sites, catch sites, and exception instances; if a method is advertised as throwing a checked exception, that should have two effects: (1) the function should handle a "throw" of the checked exception by doing something special on return (e.g. setting the carry flag, etc. depending upon the exact platform) which calling code would be required to be prepared for.
    – supercat
    Jan 8, 2013 at 18:31
  • 7
    "Without exceptions the code is full of error-checking noise.": I am not sure about this: in Haskell you can use monads for this and all the error checking noise is gone. The noise introduced by "multistate return types" is more a limitation of the programming language than of the solution in itself.
    – Giorgio
    Jun 28, 2014 at 6:50
10

For a long time OO languages, the use of exceptions have been the de-facto standard for communicating errors. But functional programming languages provide the possibility of a different approach, e.g. using monads (which I have not been using), or the more lightweight "Railway Oriented Programming", as described by Scott Wlaschin.

It is really a variant of the multistate result type.

  • A function returns either a success, or an error. It cannot return both (as is the case with a tuple).
  • All possible errors have been succinctly documented (At least in F# with result types as discriminated unions).
  • The caller cannot use the result without taking into consideration if the result was a success or a failure.

The result type could be declared like this

type Result<'TSuccess,'TFail> =
| Success of 'TSuccess
| Fail of 'TFail

So the result of a function that returns this type would be either a Success or a Fail type. It cannot be both.

In more imperative oriented programming languages, this kind of style could require a large amount of code on the caller site. But functional programming allows you to construct binding functions or operators to tie together multiple functions so error checking doesn't take up half the code. As an example:

// Create an updateUser function that takes an id, and new state
// as input, and updates an existing user.
let updateUser id input =
    validateInput input
    >>= loadUser id
    >>= updateUser input
    >>= saveUser id
    >>= notifyAboutUserUpdated

The updateUser function calls each of these functions in succession, and each of them could fail. If they all succeed, the result of the last called function is returned. If one of the functions fail, then the result of that function will be the result of the overall updateUser function. This is all handled by the custom >>= operator.

In the above example, the error types could be

type UserValidationErrorType =
| InvalidEmail of string
| MissingFirstName of string
... etc

type DbErrorType =
| RecordNotFound of int
| ConcurrencyError of int

type UpdateUserErrorType =
| InvalidInput of UserValidationErrorType
| DbError of DbErrorType

If the caller of updateUser does not explicitly handle all possible errors from the function, the compiler will issue a warning. So you have everything documented.

In Haskell, there is a do notation that can make the code even cleaner.

4
  • 2
    Very good answer and references (railway-oriented programming), +1. You may want to mention Haskell's do notation, which makes the resulting code even cleaner.
    – Giorgio
    Jun 28, 2014 at 8:08
  • 1
    @Giorgio - I did now, but I haven't worked with Haskell, only F#, so I couldn't really write a lot about it. But you could add to the answer if you want.
    – Pete
    Jun 28, 2014 at 8:28
  • Thanks, I wrote a small example but since it was not small enough to be added to your answer I wrote a complete answer (with some extra background information).
    – Giorgio
    Jun 28, 2014 at 11:01
  • 2
    The Railway Oriented Programming is exactly monadic behavior.
    – Daenyth
    Jul 20, 2016 at 12:49
5

I find Pete's answer very good and I would like to add some consideration and one example. A very interesting discussion regarding the use of exceptions versus returning special error values can be found in Programming in Standard ML, by Robert Harper, at the end of Section 29.3, page 243, 244.

The problem is to implement a partial function f returning a value of some type t. One solution is to have the function have type

f : ... -> t

and throw an exception when there is no possible result. The second solution is to implement a function with type

f : ... -> t option

and return SOME v on success, and NONE on failure.

Here is the text from the book, with small adaptation made by myself to make the text more general (the book refers to a particular example). The modified text is written in italics.

What are the trade-offs between the two solutions?

  1. The solution based on option types makes explicit in the type of the function f the possibility of failure. This forces the programmer to explicitly test for failure using a case analysis on the result of the call. The type checker will ensure that one cannot use t option where a t is expected. The solution based on exceptions does not explicitly indicate failure in its type. However, the programmer is nevertheless forced to handle the failure, for otherwise an uncaught exception error would be raised at run-time, rather than compile-time.
  2. The solution based on option types requires an explicit case analysis on the result of each call. If “most” results are successful, the check is redundant and therefore excessively costly. The solution based on exceptions is free of this overhead: it is biased towards the “normal” case of returning a t, rather than the “failure” case of not returning a result at all. The implementation of exceptions ensures that the use of a handler is more efficient than an explicit case analysis in the case that failure is rare compared to success.

[cut] In general, if efficiency is paramount, we tend to prefer exceptions if failure is a rarity, and to prefer options if failure is relatively common. If, on the other hand, static checking is paramount, then it is advantageous to use options since the type checker will enforce the requirement that the programmer check for failure, rather than having the error arise only at run-time.

This as far as the choice between exceptions and option return types is concerned.

Regarding the idea that representing an error in the return type leads to error checks spread all over the code: this need not be the case. Here is a small example in Haskell that illustrates this.

Suppose we want to parse two numbers and then divide the first by the second. So there could be an error while parsing each number, or when dividing (division by zero). So we have to check for an error after each step.

import Text.Read

parseInt :: String -> Maybe Int
parseInt s = readMaybe s :: Maybe Int

safeDiv :: Int -> Int -> Maybe Int
safeDiv n d = if d /= 0 then Just (n `div` d) else Nothing

toString :: Maybe Int -> String
toString (Just i) = show i
toString Nothing  = "error"

main = do
         -- Get two lines from the terminal.
         nStr <- getLine
         dStr <- getLine

         -- Parse each string and divide.
         let r = do n <- parseInt nStr
                    d <- parseInt dStr
                    safeDiv n d

         -- Print the result.
         putStrLn $ toString r

The parsing and the division are performed in the let ... block. Note that by using the Maybe monad and the do notation, only the success path is specified: the semantics of the Maybe monad implicitly propagates the error value (Nothing). No overhead for the programmer.

1
  • 2
    I think in cases like this where you want to print some kind of useful error message, the Either type would be more suitable. What do you do if you get Nothing here? You just get the message "error". Not very helpful for debugging.
    – sara
    Jun 27, 2016 at 8:49
0

I have become a big fan of Checked Exceptions and I'd like to share my general rule on when to use them.

I have come to the conclusion that there are basically 2 types of errors that my code has to deal with. There are errors that are testable before the code executes and there are errors that are non-testable before the code executes. A simple example for an error that is testable before the code is executed in a NullPointerException.

//... bad code below.  the runnable variable
// tries to call the run() method before the variable
// is instantiated.  Running the code below will cause
// a NullPointerException.
Runnable runnable = null;
runnable.run();

A simple test could have avoided the error such as...

Runnable runnable = null;
...
if (runnable != null)
{   runnable.run(); }

There are times in computing where you can run 1 or more tests before executing the code to make sure you are safe AND YOU WILL STILL GET AN EXCEPTION. For example, you can test a file system to be sure there is enough disk space on the hard drive before you write your data to the drive. In a multiprocessing operating system, such as the ones used today, your process could test for disk space and the file system will return a value saying there is enough space, then a context switch to another process could write the remaining bytes available to the operating system. When the Operating system context switches back to your running process where you write your contents to disk, an Exception will occur simply because there is not enough disk space on the file system.

I consider the scenario above as a perfect case for a Checked Exception. It's an Exception in the code that forces you to deal with something bad even though your code could be perfectly written. If you choose to do bad things like 'swallow the exception', you are the bad programmer. By the way, I have found cases where it is reasonable to swallow the exception but please leave a comment in the code as to why the exception was swallowed. The Exception handling mechanism is not to blame. I commonly joke that I'd prefer my heart pacemaker to be written with a language that has Checked Exceptions.

There are times when it becomes tough to decide whether the code is testable or not. For example, if you are writing an interpreter and a SyntaxException is thrown when the code fails to execute for some syntactical reason, should the SyntaxException be a Checked Exception or (in Java) a RuntimeException? I would answer if the interpreter checks the syntax of the code before the code is executed then the Exception should be a RuntimeException. If the interpreter simply runs the code 'hot' and simply hits a Syntax error, I'd say the exception should be a Checked Exception.

I will admit that I'm not always happy to have to catch or throw a Checked Exception because there are time where I'm not sure what to do. Checked Exceptions are a way to force a programmer to be mindful of the potential problem that can occur. One of the reasons why I program in Java is because it has Checked Exceptions.

2
  • 1
    I'd rather my heart pacemaker was written in a language that did not have exceptions at all, and all lines of code handled errors through return codes. When you throw an exception you are saying"its all gone wrong" and the only safe way to continue processing is to stop and restart. A program that so easily ends up in an invalid state is not something you want for critical software (and Java explicitly disallows its use for critical software in the EULA)
    – gbjbaanb
    Mar 26, 2015 at 14:27
  • Using exception and not checking them vs using return code and not checking them in the end it all yields the same cardiac arrest.
    – Newtopian
    Oct 26, 2017 at 20:30
-1

I'm currently in the middle of a rather large OOP based project/API and I have used this layout of the exceptions. But it all really depends of how deep you want to go with the exception handling and the like.

ExpectedException
- AuthorisedException
- EmptySetException
- NoRemainingException
- NoRowsException
- NotFoundException
- ValidationException

UnexpectedException
- ConnectivityException
- EnvironmentException
- ProgrammerException
- SQLException

EXAMPLE

   $valid_types = array('mysql', 'oracle', 'sqlite');
       if (!in_array($type, $valid_types)) {
           throw new ecProgrammerException(
        'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
    $type,
    join(', ', $valid_types)
    );
}
4
  • 11
    If the exception is expected, it isn't really an exception. "NoRowsException"? Sounds like control flow to me, and therefore a poor use of an exception. May 31, 2011 at 16:09
  • 1
    @qes: It makes sense to raise an exception whenever a function is unable to calculate a value, e.g. double Math.sqrt(double v) or User findUser(long id). This gives the caller the freedom to catch and handle errors where it is convenient, instead of checking after each call. May 31, 2011 at 20:17
  • 1
    Expected = control flow = anti-pattern of exception. Exception shouldn't be used for control flow. If it's expected to produce error for specific input, then it just be passed a a part of return value. So we have NAN or NULL.
    – Eonil
    Dec 4, 2013 at 5:15
  • 1
    @Eonil ...or Option<T> Apr 26, 2014 at 9:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.