The best way to handle exceptions

So, it’s well known that you shouldn’t have code that looks like this (examples in ruby and ruby-like pseudo code, but they’re trivially translatable to any other language):

begin
   do stuff
rescue
end

i.e. you’re swallowing the exceptions because they scared you and you wanted to hide them. This is naughty.

A more subtle trap people fall into is the following example from the rails boot.rb (this isn’t a rails specific problem. I see it everywhere)

    def load_rails_gem
      if version = self.class.gem_version
        gem 'rails', version
      else
        gem 'rails'
      end 
    rescue Gem::LoadError => load_error
      $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION
      exit 1
    end 

Let’s consider the results of this rescue block: A generic error message is printed, and we exit with a non-zero status code.

Now let’s consider the results of not having this rescue block: A specific error message is printed, we exit with a non-zero status code and we get a stack trace telling us exactly what went wrong.

So by including this rescue, we have lost information. Often this information doesn’t matter, but as it turns out in this case it does: If you have a gem version clash where rails depends on a different version of a gem that has already been loaded you will get a Gem::LoadError and, consequently, a very misleading error message.

I don’t want to pick on rails. Well, correction. I don’t want to pick on rails here. This post is actually inspired by a similar incident at work: There was a piece of code that basically looked like this:

begin
  connect to server
rescue
  STDERR.puts "Could not connect to server"
  exit
end

And were getting very puzzling errors where we were sure all the details were correct but it was failing to connect to the server. Once we deleted the rescue code it was immediately obvious why it was failing (if you care, the reason was that we weren’t loading the config correctly so it was trying to connect with some incorrect default values).

Which brings me to the point of this article: The best way to handle exceptions is not to handle them. If it’s not an exception you can reasonably recover from, the chances are pretty good that the default behaviour is more informative than the “helpful” code you were going to write in order to catch and log the error. So by not writing it you get to have less code and spend less time debugging it when it inevitably goes wrong.

This entry was posted in Uncategorized on by .

20 thoughts on “The best way to handle exceptions

  1. Colin Howe

    I think that the one piece of value you can add when handling exceptions is to add more information to the stack trace. In Java you might do something like:

    catch (Exception e) {
    throw new RuntimeException(“Failed to connect to server [” + serverName + “]”, e);
    }

    As sometimes the exceptions caught don’t have the correct amount of information (e.g. null pointer exceptions)

    1. david Post author

      This is another example of something I wish people wouldn’t do, though it’s less severe. The problem with this approach is that it changes the type of exception being thrown, which makes fine grained error handling further up the line harder to do.

      I’d prefer such code be written more like:

      catch(Exception e){
      System.err.println(“Failed to connect to server [” + serverName + “]”);
      throw e;
      }

      though this isn’t perfect either as it leads to weird error message inversions. Also Java checked exceptions make this more problematic.

      1. Colin Howe

        True, it would be nice to preserve the type of exception being thrown. You almost want:

        catch (Exception e) {
        e.addInformation(“Server name”, serverName);
        throw e;
        }

        Or, a way for the language to do it for you in places you specify:
        @AddInfoToException
        public void connect() { …

        Might add all local variables to the exception when this annotation is present

      2. joey

        One way of adding information is to wrap the caught exception:

        catch (Exception e) {
        throw new IOException(“Could not connect to ” + serverName, e);
        }

      3. Dim

        So… we’re doing fine grained error handling but we can’t be bothered to inspect inner exceptions?

      4. david Post author

        “Can’t be bothered to catch inner exceptions” is an amusing spin on “Have no facility to catch based on the type of inner exceptions”

  2. Alaric Snell-Pym

    Yes. It’s a common misconception that one has to handle exceptions. People look at the return value and exception signature of a function as one thing – and decide how to handle the result of that call.

    Exceptions should only be caught in three places, off of the top of my head:

    1) Where you can do something useful about the exception (eg, try something else)

    2) Where you can’t let the failure rise any further (eg, you’re looping through a number of entries in a queue, and if one fails you want to log that and start on the next, rather than giving up on the whole queue)

    3) Where you’re protecting the user from the grungy details. This one is easy to get wrong. Yeah, the QA team complain that your app gives a scary error message full of stacktrace. So catch that exception, pop up a little dialog box with a single line of error message in it – but then have a button that opens up your mail client with an email to [email protected] with the backtrace in it, or equivalent.

    1. david Post author

      Agreed on all of these. One additional case where it’s sometimes necessary: When you’re doing something funny with control flow. e.g. because your code is written in CPS, or because you need to pass things between threads. e.g. the RabbitMQ Java client runs most of its handling code in a MainLoop thread, and when things go wrong it needs to transmit the exceptions to the calling threads rather than just have that one die.

  3. knugen

    two types of exceptions:

    1) user generated exceptions.

    these are the ones that a program should try to catch. IO, FileNotFound or whatever, since these
    usually don’t stem from a programming error. a missing network connection can also be contributed to the user :-)

    2) runtime exceptions, generated by some funky code

    catching these will lead to a hopeless state, since very few programming languages opts for a recompilation of bad code based on exceptions, which is really what we would want. we need to rewrite the code, nothing else will do..

    1. david Post author

      No. Most of the time with things you’ve classed as “user generated” exceptions the correct behaviour is still to let it crash. If you’ve failed to connect to the database, or to load a config file, or a rest API you’re talking to has gone away, or any other number of cases, the behaviour you will add when trying to catch and handle this is significantly less helpful than the default.

      There are *some* instances of these where you want more specific error handling: The user has selected a file in a dialog that doesn’t exist, or tried to connect to a host that isn’t there, etc. But you can’t distinguish these based on the type of the exception, only on a case by case basis, and it’s usually fairly obvious which ones these are – they’re the ones that are actions in direct response to user input which we have not yet validated. Your default should still be to let it fail.

      1. knugen

        if you write programs for programmers, the best way to handle exceptions is always to let things crash, with a stacktrace or whatever. but some people write programs for people who are not programmers, so when the user has generated an exception, you would typically to this:

        if( i still can go on with this error )
        do some handling..
        else
        write polite ‘sorry missing input file, or whatever’ and exit

        this is why exceptions are there in the first place, letting stuff crash is not a
        ‘new way of handeling exceptions’, it’s the old way..

      2. david Post author

        Note that I specifically listed that as a case where actually a friendly error message is more appropriate. Also note that you are the only person who has made “new” an issue. I don’t care about new, I care about correct.

        However a much larger category of error messages, particularly in server side programming, which you were claiming as “user generated” are in fact entirely the fault of the programmer and are better left untouched without obfuscation (and even in the case where they are caused by the user you damn well better be logging the actual reason for when you screw up)

  4. Chris Chiesa

    One of the things I’ve always disliked about the C++ stream I/O facilities is that they throw away exactly this sort of detailed error information. If you try to open an fstream to a file, and it fails, there seems to be no way to get the details of why it failed. That’s inexcusable.

  5. Matt

    “So by including this rescue, we have lost information.”

    In Java you can specify a cause in .Net an InnerException, so you don’t lose any information but add to it. I’m sure other platforms have similar facilities.

  6. Dave Hall

    Something worth pointing out here is the new error handling mechanisms that were introduced in Python 3.1. This new feature made the Java-style exception wrapping / chaining built-in to the language.

    Thus, in Python 3, it’s now okay to write:

    try:
    connect_to_server()
    except IOError as ex:
    raise CouldNotConnectToServerError(“Could not connect to the server”)

    This code snippet, rather than hiding the original error, actually adds a nicer user-friendly message to the stack trace. The resultant output, if allowed to propagate and crash the program, looks something like this:

    CouldNotConnectToServerError “Could not connect to the server”
    Line 4 in server.py

    This exception was caused by IOError “DNS did not resolve”
    Line 354 in dns.py
    Line 200 in net.py
    Line 456 in foo.py

    And so on. Yes, it’s basically the same as the Java wrapping method, but it encourages a workflow whereby you don’t have to worry about hiding debugging information by raising your own, more helpful exceptions. Rather than one giant stack trace, you have several bitesize ones. Obviously, you can still just print to stderr and exit, but at least now you have to try a bit harder to hide valuable information.

  7. @ndy

    I think that this feeds into higher level design issues. Often you don’t want the program to crash. Perhaps because you’ve built it as a big monolith and have lots of state that you need to save; or whatever. If you’re using a multiprogramming approach with lots of tools that do one thing well then it matters less if that one program crashes as opposed to the entire stack.

  8. Pingback: Best of drmaciver.com | David R. MacIver

Comments are closed.