Exceptions: The Right Way

Are exceptions evil? Many developers don’t like them, because they’re not easy and some times they have to spend hours on discussions about which of them should they use, how they should handle them, etc. This post isn’t about how good or bad exceptions are or if you should use them. Instead, we’ll focus on their advantages, disadvantages and how you can use them the right way, especially when you write libraries.

1. Advantages

  • You can’t ignore them
  • The error handling code is separated from the regular code
  • The finally block can be used for resources clean-up
  • Errors can propagate up the call stack
  • Errors grouping (e.g. FileNotFoundException can be a subclass of IOException)
  • Exceptions are the only way to return errors from a constructor

2. Disadvantages

  • They are misused
  • They are overused
  • They are invisible: When looking at some code it isn’t obvious which things can throw exceptions and which can’t

3. Good practices

There are several good practices that are used by developers and teams, in order to prevent exceptions misusage and over usage. Let’s see the most important ones that can help you make better decisions, improve the quality of your code and make your colleagues happier.

Prefer specific than generic exceptions

Specific exceptions make your class/method easier to understand. If standard exceptions don’t fit in your business logic, feel free to create custom, more specific, ones.

Catch the more specific exceptions first

When an exception is thrown, the first catch block that matches it is executed. If you catch a generic exception first, the more specific one won’t be executed. For example:

try {
    throw new InvalidArgumentException('Message');
} catch (Exception e) {
    // this block will be executed
} catch (InvalidArgumentException e) {
    // this block won't be executed
}

Use clear messages

The developer who uses your code must be able to understand what went wrong when an exception is thrown. So, error messages like An error occurred are not helpful at all. Always provide messages someone can read and understand.

Document your exceptions

As I mentioned on the disadvantages, exceptions are invisible and that’s a very good reason for documenting them. Usually, the way to do that is by using special annotations, like the throws. For example:

/**
 * @throws InvalidArgumentException if the provided argument is not of type 'array'.
 */

Wrap exceptions

Ideally, you wouldn’t want to propagate every exception the code you use throws, especially if it comes from other libraries. In such cases, wrapping the exception in one that makes more sense for your business domain is a good idea. For example, let’s say your code uses a third party HTTP client that can throw a \AwesomeHttpClient\Exceptions\TimeoutException. Do you think you should expose that information to the callers? Should they know what library your code uses and handle all of its exceptions? What if you decide to replace that library with another one? For example:

try {
    // code that throws \AwesomeHttpClient\Exceptions\TimeoutException
} catch (\AwesomeHttpClient\Exceptions\TimeoutException e) {
    throw new \MyCompany\Exceptions\TimeoutException('Timeout message');
}

Log exceptions

Exceptions are supposed to be used in exceptional cases, so you should log them when they happen. Ideally, this should happen at a high level, where their handling happens.

Use a finally block

The finally block is perfect for resources cleanup, like database connections, close open files, etc. If the code in the finally block can throw an exception, you have to use a try/catch block inside it, in order to handle it. Pseudocode example:

File f = new File();
try {
    f.open('filename', 'r');
    // code that can throw exceptions
} finally {
    f.close();
}

4. How not to use exceptions

Catch and ignore

Catching and ignoring an exception is something you should avoid. Unfortunately, some times you don’t have a choice. For example, when you use a method that throws an exception, but your code execution must be continued, because of the business logic.

try {
    // code that throws an Exception
} catch (Exception e) { }

Log and throw

In general, low levels must throw the exception and high levels handle it. So, don’t log and throw, since the caller will probably log it, too.

try {
    // code that throws an Exception
} catch (Exception e) {
    log.error(e.getMessage());
    throw e;
}

Throw a generic exception

Throwing a generic exception may be seems fine, but think about what will happen in the future when you’ll throw a more specific exception. The callers will never discover that exception, because they already catch the generic one.

function foo()
{
    if (condition) {
        throw new Exception('Message here');
    }

    if (newCondition) {
        throw new MySpecificException('Message here');
    }
}

try {
    foo();
} catch (Exception e) {
    // this catch block catches both Exception and MySpecificException
    // ideally, we should catch and handle the MySpecificException exception first
}

Throw the same exception type

I recently wrote an integration with a very popular application that returns the value 1 for most of the errors. Can you guess what I had to do in order to handle different errors in a different way? (hint: grep). Nevertheless, it might make sense to throw the same exception type sometimes (e.g. InvalidArgumentException).

function foo()
{
    if (condition1) {
        throw new MyException('Message1');
    }

    if (condition2) {
        throw new MyException('Message2');
    }

    if (condition3) {
        throw new MyException('Message3');
    }

    if (condition4) {
        throw new MyException('Message4');
    }
}

Throw too many exceptions

If you throw a lot of different exceptions, the caller must handle all of them. That may lead them to catch the generic Exception, which is not a good practice.

Throw an unnecessary exception (example in PHP)

Exceptions must be used in exceptional cases. Always consider alternatives, before throwing an exception. Can the code be usable without that exception? If so, there’s no reason to throw it. Wouldn’t returning an empty array when the grep function doesn’t match any lines, instead of throwing an exception be a better alternative in the following code?

With exception:

function grep($search, $lines) { 
    $result = []; 
    foreach($lines as $line) { 
        if (strpos($line, $search) !== false) { 
            $result[] = $line; 
        } 
    }

    if (!$result) {
        throw new \Exception('No lines match');
    }

    return $result;
}

try { 
    $result = grep('test', []); 
} catch (\Exception $e) { 
    echo $e->getMessage(); 
}

Without exception:

function grep($search, $lines) { 
    $result = []; 
    foreach($lines as $line) { 
        if (strpos($line, $search) !== false) { 
            $result[] = $line; 
        } 
    }

    return $result;
}

$result = grep('test', []); 
if (!$result) {
   echo 'No lines match'; 
}    

Remember

  • Exceptions thrown by a class or function are part of its interface.
  • Throwing exceptions is easy, but handling them is hard.
  • Exceptions are invisible. When looking at some code it isn’t obvious which things can throw exceptions and which can’t.
  • Your code has users, so it has User Experience. If you throw a lot of (different) exceptions, the caller must handle all of them. That may lead them to catch the generic Exception, which is not a good practice. Before you start throwing a bunch of them, put yourself in the shoes of those who will use your code.
  • Think twice before you throw an exception. Can the code work without it?

Leave a Reply

Your email address will not be published. Required fields are marked *