Clean Apex: Should you return null, empty or throw an exception?

đź’ˇ
This is a FREE preview of Chapter 5 of my upcoming book: Clean Apex Code, Software Design for Salesforce Developers

A common source of debate is whether you should ever return null, let's look at this simple method as an example

public User findUser(String username) {

    //some logic here to find the user
    
    return foundUser;
    
}

What should this method return if the user is not found? I encourage you to pause and think about it for a while before you keep reading.

If we return null, the caller will have to check if the return value is not null before doing something with that value, otherwise a Null Pointer Exception will be thrown, for example

User existingUser = findUser('testuser@company.com');

if(existingUser != null){
    //do something with the user
}

Another option would be return an empty User object, like this (note the below is pseudo-code)

public User findUser(String username) {

    User foundUser = new User();

    if(weFoundTheUser){
        foundUser = theUser;
    }

    return foundUser;    
}

If the if condition is not true, we will return foundUser as it was first instantiated, which is a non-null but empty object. In this case, the caller still needs to check if the User isn’t empty, for example

User existingUser = findUser('testuser@company.com');

if(existingUser?.getPopulatedFieldsAsMap().isEmpty() == false){
   //do something with the user
} 

Here, we use the getPopulatedFieldsAsMap() method from the sObject class to get a map of all the fields that have been populated in memory for this object. If the map is not empty, then the user was found. This is not a good pattern though as it forces the caller to mix different levels of abstraction and to know internal details about the logic inside of findUser().

It’s also not guaranteed to work, because default field values can be present in the sObject if you create it with the newSObject method of the sObjectType class instead of the new operator. For example

Boolean loadDefaultValues = true;

User existingUser = (User)User.sObjectType.newSObject(null, loadDefaultValues);

if(existingUser?.getPopulatedFieldsAsMap().isEmpty() == false){
   System.debug('user is NOT empty');
}
else{
    System.debug('user is empty');
}

In the above example, user is NOT empty is printed to the debug log because default values are loaded into memory.

The last option would be to throw an exception, like this (I will talk about when it makes sense to throw exceptions in the next chapter)

public User findUser(String username) {

    if(weDidntFindTheUser){
        throw new NoDataFoundException('User not found');
    }
}

In this case, the caller would have to wrap the call to findUser() inside a try/catch block, like this

try {
    User existingUser = findUser('testuser@company.com');
} catch (NoDataFoundException e) {
    // log the exception or rethrow it
}

We can see that no matter what approach we take, the caller will always have to check if the returned value is what it actually expected. There is no way around this.

For this particular example, I think returning null makes sense because you asked for a value that isn’t there. This would follow convention used in maps, where using the Map.get(key) method returns null if the key doesn’t exist in the map.

On the other hand, if the method is meant to return multiple records, then returning an empty list would be more appropriate because it follows the convention used in SOQL queries, for example

// if there are no accounts, the list will be empty
// not null
List<Account> accounts = [SELECT Id FROM Account];

Therefore, the following code would make sense; we return a list that may or may be empty.

public List<User> findUsers(Set<String> usernames) {

    if(usernames == null){
        throw new IllegalArgumentException('Usernames set cannot be null');
    }

    List<User> users = [SELECT Id, Name FROM User WHERE Username IN :usernames];

    //this returns an empty list if no users are found
    return users;
}

What is the conclusion then? Should we return null? I recommend you follow these guidelines

  • Return null if a method is supposed to find something, and that object does not exist
  • Throw an exception if an exceptional condition occurs—not finding an object is usually not exceptional.
  • If a method is meant to return a list, return an empty list if the records cannot be populated for whatever reason; this follows the SOQL convention.

Subscribe for exclusive Salesforce Engineering tips, expert DevOps content, and previews from my book 'Clean Apex Code' – by the creator of HappySoup.io!
fullstackdev@pro.com
Subscribe