Almost every developer working with the Salesforce platform will have encountered the dreaded phrase Attempt to de-reference a null object. At its heart, this is one of the simplest errors to both generate and handle effectively, but its error message can cause great confusion for new and experienced developers alike, as it is often unclear how the exception is occurring.
Let’s start by discussing in the abstract form how the error is generated. This is a runtime error, caused by the system attempting to read data from memory where the memory is blank. Apex is built on top of the Java language and uses the Java Virtual Machine (JVM) runtime under the hood. What follows is a highly simplified discussion of how Java manages memory, which will help us to understand what is happening behind the scenes.
Whenever an object is instantiated in Java, it is created and managed on the heap, which is a block of memory used to dynamically hold data for objects and classes at runtime. A separate set of memory, called the stack, stores references to these objects and instances. So, in simplistic terms, when you instantiate an instance of a Person
class called paul
, that instance is stored on the heap and a reference to this heap memory is stored on the stack, with the label paul
. Apex is built on Java and compiles down to Java bytecode (this started after an update from Salesforce in 2012), and, although Apex does not utilize a full version of the JVM, it uses the JVM as the basis for its operations, including garbage and memory management.
With this in mind, we are now better able to understand how the two most common types of NullPointerException
instances within Apex occur: when working with specific object instances and when referencing values within maps.
Exceptions on object instances
Let’s imagine I have the following code within my environment:
public class Person {
public String name;
}
Person paul;
In this code, we have a Person
class defined, with a single publicly accessible member variable. We have then declared a variable, paul
, using this new data type. In memory, Salesforce now has a label on the stack called paul
that is not pointing to any address on the heap, as paul
currently has the value of null
.
If we now attempt to run System.debug(paul.name);
, we will get an exception of type NullPointerException
with the message Attempt to de-reference a null object. What is happening is that the system is trying to use the paul
variable to retrieve the object instance and then access the name
property of that instance. Because the instance is null
, the reference to this memory does not exist, and so a NullPointerException
is thrown; that is, we have nothing to point with.
With this understanding of how memory management is working under the hood (in an approximate fashion) and how we are generating these errors, it is therefore easy to see how we code against them—avoid calling methods and accessing variables and properties on an object that has not been instantiated. This can be done by ensuring we always call a constructor when initializing a variable, as shown in the following code snippet:
Person paul = new Person();
In general, when developing, we should pay attention to any public methods or variables that return complex types or data from a complex type. A common practice is simply to instantiate new instances of the underlying object in the constructor for any data that may be returned before being populated.
Exceptions when working with maps
Another common way in which this exception presents itself is when working with collections and data retrieved from collections—most notably, maps. As an example, we may have some data in a map for us to use in processing unrelated records. Let’s say we have a Map<String, Contact> contactsByBadgeId
instance that allows us to use an individual’s unique badge ID string to retrieve their contact record for processing. Let’s try to run the following:
String badBadgeId = 'THIS ID DOES NOT EXIST';
String ownerName = contactsByBadgeId.get(badBadgeId).FirstName;
Assuming that the map will not have the key value that badBadgeId
is holding, the get
method on the map will return null
, and our attempt to access the FirstName
property will be met with NullPointerException
being thrown.
The simplest and most effective way to manage this is to wrap our method in a simple if
block, as follows:
String badBadgeId = 'THIS ID DOES NOT EXIST';
if(contactsByBadgeId.containsKey(badBadgeId)) {
String ownerName = contactsByBadgeId.get(badBadgeId)
.FirstName;
}
By adding this guard clause, we have proactively filtered out any bad keys for the map by removing the error.
As an alternative, if we were looping through a list of badge IDs, like this:
for(String badgeId : badgeIdList) {
String ownerName = contactsByBadgeId.get(badBadgeId).FirstName;
}
We could also use the methods available on set collections to both potentially reduce our loop size and avoid the issue, as follows:
Set<String> badgeIdSet = new Set<String>(badgeIdList).retainAll(contactsByBadgeId.keySet());
for(String badgeId : badgeIdSet) {
String ownerName = contactsByBadgeId.get(badBadgeId).FirstName;
}
In the preceding example, we have filtered down the items to be iterated through to only those in the keySet
instance of the map. This may not be possible in many instances, as we may be looping through a collection of a non-primitive type or a type that does not match our keySet
instance. In these cases, our if
statement is one solution. We can also use a feature called safe navigation to allow Apex to assist us in avoiding these issues.
Safe navigation operator
The discussion and methods shown in the preceding sections provide ways for us to code to avoid a NullPointerException
instance occurring through the way we structure our code, which can also assist in performance. For example, the second set of code for looping through a pre-filtered list of IDs based on the map’s keys will avoid unnecessary looping and operations. Since the first edition of this book was published, Salesforce has added in a safe navigation operator that helps remove a lot of the null
checks we previously had to perform.
The safe navigation operator (?.
) will return null
if a value is unavailable (when previously a NullPointerException
instance would occur) and return a value if one is available. We can rewrite our badge ID retrieval code as follows:
String badgeId = 'THIS ID DOES NOT EXIST';
String ownerName = contactsByBadgeId.get(badBadgeId)?.FirstName;
In this instance, if badgeId
is a key on the map and can be retrieved, then the FirstName
value for the contact is assigned to ownerName
. If the value of badgeId
is not a valid key, then ownerName
is set to null
. We can see some more examples in the following code block:
Integer employeeCount = myAccount?.NumberOfEmployees; //returns null if myAccount is not an Account instance
paul?.getContactRecord()?.FirstName; //returns null if paul is null or
the getContactRecord() method returns null
String accName = [SELECT Name FROM Account WHERE Account_Reference__c
= :externalAccountKey]?.Name; //returns null if no Account record is
found by the query
The safe navigation operator is an extremely useful operator to help developers in minimizing errors; however, it should not be considered a panacea. You may unintentionally cause further NullPointerException
instances within your code by not correctly verifying whether a null
value has been returned. In the following example, we take our badge ID code to retrieve the name of the badge’s owner:
String ownerName = contactsByBadgeId.get(badBadgeId)?.FirstName;
We may then use this value within a component or page to display a greeting to the individual. If a null
value has been returned, this can lead to some unexpected messages:
String ownerName = contactsByBadgeId.get(badBadgeId)?.FirstName;
String welcomeMessage = 'Hello there ' + ownerName;
//"Hello there null" would be displayed as a greeting.
It is important, therefore, that within your code base and among your development team an agreement is reached as to where the responsibility for these checks lies within the code to ensure that unintended consequences do not occur.
In general, most NullPointerException
instances occur when a premature assumption about the availability of data has been made—for example, that the object has been instantiated or that our map contains the key we are looking for. Trying to recognize these assumptions will assist in avoiding these exceptions, going forward. We can also use the safe navigation operator in instances where we want to ensure safety to allow the code execution to continue, but must then be aware of checking for null
values within our code. With this in mind, let us now look at how we can effectively bulkify our Apex code.