Should methods return null or throw an exception?

As API authors, one thing we have to think about is, what happens when a method cannot fulfill its contract? Should it return null or throw and exception? Sometimes we write both the API and its consumer. Our job—especially if your API will be used by developers that aren’t us—is to make that API as easy to use and understand as possible. While you might have heard the phrase, “use exceptions only for exceptional conditions”1, if done correctly, throwing and catching exceptions can ensure a level of correctness in our programs.

To make this discussion concrete, suppose we’re to represent a vending machine. We create a VendingMachine interface and code away. When we get to the vend method, we have some decisions to make.

public interface VendingMachine {
    Candy vend();
}

(We’ll ignore this method’s parameters since they get in the way of this discussion.)

Vending machines in the real world run out of candy far too often! Our virtual vending machine is no different, so what should this method do when the vending machine is out of candy? What should the contract be?

We will think about this both from the point of view of us, the author, and our consumer, who has to live with our decision.

Let’s walk through the options.

Option 1

One option would be to return null:

public interface VendingMachine {
    /**
     * Returns candy, or {@code null} if this machine is out of
     * candy.
     *
     * @return Candy or {@code null}.
     */
    Candy vend();
}

This seems reasonable and it is a common pattern. But what are the benefits?

  • API is concise

So far, so good. Let’s also look at how this will be consumed:

Candy candy = vendingMachine.vend();

if (candy != null) {
    // do work.
} else {
    // handle the problem.
}

At first glance, everything seems fine. This is an equally common pattern. But let’s look closer at what our poor consumer has to do to use our API correctly:

  • They have to know to check for null. Not only do they have to read the documentation, but they have to remember to do the check. Neither the language nor the API does anything to help them out. If the null check was ignored this code would certainly look cleaner, but it would hide a dangerous NullPointerException (NPE).
  • The candy variable is declared outside of the if check, which means it might accidentally be referenced outside of the if check and cause a NPE.

We can do better than this.

Option 2

We could add a method that would indicate whether or not a call to vend will be successful. Let’s see what this looks like.

public interface VendingMachine {
    /**
     * Returns {@code true} if this vending machine has candy.
     *
     * @return {@code true} if this vending machine has candy.
     */
    boolean hasCandy();

    /**
     * Returns candy, or {@code null} if this machine is out of
     * candy.
     *
     * @return Candy or {@code null}.
     *
     * @see hasCandy()
     */
    Candy vend();
}

This solution is not as elegant as option 1: we went from one method representing one idea, to two methods representing one idea. This puts a greater burden on you, the author.

  • You must ensure these two methods are consistent. If one
    changes, the other must change too.

Lets see what our consumer will have to do to use this correctly.

if (vendingMachine.hasCandy()) {
    Candy candy = vendingMachine.vend();
    // do work.
} else {
    // handle the problem.
}

This looks only moderately improved form option 1, let’s evaluate it.

  • The candy variable is guaranteed only by the API (and not the language) to not be null when called inside this if block. This is an improvement over the last iteration.
  • Consumers have to remember to call hasCandy(). This trades a null check for a method call, but ultimately still relies on our consumer reading the documentation, so we haven’t gained any ground here.

This solution might even be worse. Notice that this new API could be invoked just like the API in option 1 and it would still be valid. Heck, it might even be preferred since it doesn’t rely on hasCandy being implemented correctly. Worse still, a consumer may be overly cautious and do both checks:

if (vendingMachine.hasCandy()) {
    Candy candy = vendingMachine.vend();
    // Let's be extra safe!
    if (candy != null) {
        // do work.
    }
} else {
    // handle the problem.
}

While marginally better than option 1, I think we can do even better.

Option 3

What if vend threw an exception when the machine was out of candy?

First, we should discuss when exceptions should be used. I don’t like the “exceptions for exceptional circumstances” heuristic, however, because it is too vague. After all, there’s nothing exceptional about a vending machine that’s out of candy! I prefer this definition: use exceptions when a contract cannot be fulfilled. Using this definition, its more clear that vend should throw an exception if it cannot return a Candy.

Next, we have to decide if we’re throwing a checked or unchecked exception. A quick rule of thumb is:

  • Unchecked (runtime) exceptions are for logic errors. These exceptions do not have to be declared by the author, and a consumer would have to rewrite the code to fix such an error.
  • Checked exceptions are for programming errors. An author must declare these exceptions and a consumer must handle them. Furthermore, an author has to be reasonably sure that a consumer could do something about the problem.

We’ll make vend throw a checked exception because we’ll expect him to be able to do something about it, such as requesting a refill. By introducing an EmptyVendingMachineException, our interface now looks like this:

public interface VendingMachine {
    /**
     * Vends Candy.
     *
     * @return Your favorite candy.
     *
     * @throws EmptyVendingMachineException if this machine is
     *     out of candy.
     */
    Candy vend() throws EmptyVendingMachineException;
}

Written this way, we don’t have to maintain two methods that represent the same concept, our intent is clearly stated and enforced by the language.

  • API is concise.
  • The contract is enforced by the language.

To really see why this solution is superior, let’s see how this looks to the consumer:

try {
    Candy candy = vendingMachine.vend();
    // do work.
} catch (EmptyVendingMachineException e) {
    // handle the problem.
}

A lot of potential mistakes have been handled for the consumer:

  • The candy variable will never be null inside the try block. This is similar, but stronger, than option 2’s guarantee, and with the additional guarantee that candy can only be in a try block.
  • The consumer can no longer forget to handle the error case. They may not like it, but if this is really an error they should be handling, they’ll thank you later.

This solution is beneficial to you the author and your consumer alike.

At this point you might be thinking, “but isn’t throwing and catching an exception slower than just doing a null check?” and you’d be right: checking for null is marginally faster than throwing and catching an exception. However, doesn’t that sound like premature optimization to you? We should first strive for program correctness.

Conclusions

While this assumes a consumer will handle the problem—whether null or exception—as soon as it occurs, a similar line of reasoning applies if the problem is passed off to the next consumer (e.g., either null is returned form the consumer’s method or it throws another exception); the problem has to be dealt with somewhere.

  • When methods cannot fulfill their contracts, they should throw and exception.
  • When a contract violation can, must, or should be handled by the consumer’s code, the method should throw a checked exception.
  • Methods should never return null to represent contract failure.

Null values make programming harder, and I would encourage everyone to start writing code that avoids using them.

References

  1. Effective Java, 2nd Edition. Joshua Bloch. Item 57.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s