Returning Null vs Exception

When designing an API, developers may always question what to set as a returned value when an operation cannot fulfill its contract? I don’t know what do you think? Should we return null? Or should we throw an exception? We write and consume APIs every day! So what do you do in this case? As a user of some API, I want to use an easy and understandable API. Also, as an author, the API must be easy to write, read, and maintain.

Hmm. Well, I like the idea of throwing an exception instead of returning null or other error values. I would argue that a null return complicates the control flow in an application as well as injects ambiguity.

Let’s get some code to show my point. We have CoffeeMachine interface and we need to write its implementation.

public interface CoffeeMachine {
    Coffee buy();
}

So we have some choices to make. Let’s think about this API and how another developer can work with it?

Scenario #1:

At the provider

In the implementation we return null;

▲ It is a common pattern and an easy solution.

At the consumer

Let’s also look how we can consumed this API:

if (coffeeMachine.buy() != null) {
    Coffee coffee = coffeeMachine.buy();
    // do the rest of thw work
} else {
    // handle the problem.
}

▼ Developers need to check for null.

▼ The scope of coffee is the block if.

▼ The null injects ambiguity into the calling application.

What does it mean null?

Ambiguity can raise when the buy is method working correctly but one information is missing. Imagine, it is about a stupid thing not in the database or something technical and not functional or vice versa. Here, null winds up with multiple meanings, resulting in the ambiguity.

Scenario #2:

At the provider

We decided to modify the API and add a method to check if the machine is working or not to buy coffee. The API becomes:

public interface CoffeeMachine {
    Coffee buy();
    boolean isWorking();
}

▼ Bad design where we have two methods to describe one operation.

▼ The author needs to work more on the code.

▼ The machine can have many different states but here we can get one single state.

At the consumer

The consumer will need to check if the machine is working before calling the buy method.

if (coffeeMachine.isWorking() != null) {
    Coffee coffee = coffeeMachine.buy();
    // do the rest of thw work
} else {
    // handle the problem.
}

▲ The coffee is guaranteed by the API inside the if block. It is better than scenario #1.

▼ The developer may find himself in scenario #1 when he’ll forget to call isWorking(). He always needs to check if the machine is working.

▼ Developers may forget and check the coffee variable again.

if (coffeeMachine.isWorking() != null) {
    Coffee coffee = coffeeMachine.buy();
    if (coffee != null) {
    	// do the rest of the work
    }
} else {
    // handle the problem.
}

▼ Scenario #1 and scenario #2 have the same structure of code if..else.

This means one thing: lack of consistency throughout the application.

Scenario #3:

At the provider

We’re taught that an exception is only supposed to be raised if something unexpected occurs. So developers will be in scenario #1 and #2 all the time.

So we decided to throw an exception MachineExcption in the implementation.

public interface CoffeeMachine {
    Coffee buy() throws MachineExcption;
}

▲ API is concise.

In other words, the API must throw an exception when there’s no coffee which means the contract is not respected.

At the consumer

The solution is cleaner than the other scenari.

try {
    Coffee coffee = coffeeMachine.buy();
    // do the rest of the work
} catch (MachineExcption ex) {
    // handle the problem.
}

▲ The coffee variable will never be null inside the block try. This solution is better than in scenario #2.

▲ No need to check if coffee is null or not, or check if the machine is working or not.

▲ This solution is advantageous to the author and the consumer alike.

Before You Go

Conclusion

Null values make programming more difficult. Throwing an exception has many benefits: Cleaner control flow in your calling code; Removes ambiguity of what “null” means; Improved consistency between method behavior in an application.