9

I've observed some strange behavior with null sObjects which are members of an Apex class. Consider the following anonymous block:

public class test
{
    public Contact tc;
    public test(){}
}

try
{
    test r = new test();
    System.debug('r: ' + r);
    System.debug('r.tc: ' + r.tc);
    system.debug('r.tc.firstname: ' + r.tc.firstname);//Line 12
    System.debug('r.tc.ID: ' + r.tc.ID);//Line 13
}
catch (NullPointerException ex)
{
    System.debug(ex.getLineNumber());
}

One would expect line 12 to throw a NullPointerException. However, the actual debug log looks like this:

17:12:04.1 (3371306)|USER_DEBUG|[10]|DEBUG|r: test:[tc=null]
17:12:04.1 (3493691)|USER_DEBUG|[11]|DEBUG|r.tc: null
17:12:04.1 (3583663)|USER_DEBUG|[12]|DEBUG|r.tc.firstname: null
17:12:04.1 (4162595)|USER_DEBUG|[17]|DEBUG|13

Strangely, the exception isn't thrown until line 13, where we try to retrieve the ID. After some more testing, I've found the following:

  1. Only sObjects seem to exhibit this behavior when inner members - if another custom class is defined, it throws as expected.
  2. Only the ID field and method calls seem to throw - so, r.tc.get('FirstName') will throw.
  3. If the sObject is assigned to a free sObject variable, fields will throw as expected, so Contact s = r.tc; s.FirstName throws an exception.

So, the question is, is this behavior documented, or is it a bug?

9

It's a feature (mostly). The most general use is to allow you to bypass checking for null pointers from query results. For example, consider the following code:

for(Contact record:[SELECT Account.Name FROM Contact WHERE AccountId = NULL]) {
    System.debug(record.Account.Name);
}

You'll get a bunch of null values in your debug logs (if you have any contacts without accounts), even though Account itself is also null.

Note that this doesn't work with method calls; once you try to call a method, you better have a non-null object to work with. For that reason, the following code won't work:

for(Contact record:[SELECT Account.Name FROM Contact WHERE AccountId = NULL]) {
    System.debug(record.getSObject('Account').get('Name'));
}

As far as I know, this isn't documented anywhere[citation needed], but it's always worked this way.

This is in the Accessing sObject Fields Through Relationships documentation:

The expression c.Account.Name, and any other expression that traverses a relationship, displays slightly different characteristics when it is read as a value than when it is modified:

When being read as a value, if c.Account is null, then c.Account.Name evaluates to null, but does not yield a NullPointerException.

This design allows developers to navigate multiple relationships without the tedium of having to check for null values.

When being modified, if c.Account is null, then c.Account.Name does yield a NullPointerException

Usually, a developer runs in to this by accident, and ends up with the opposite assumption as the developer who first gets bitten by NullPointerException (e.g. starts to check for nulls on everything).

I suspect that the reason why the NullPointerException happens on "line 13" is actually a defect in the run-time environment[citation needed], since it actually behaves differently than other SObject fields and relationships. In general, you should always initialize a variable, because NullPointerException is rather easy to avoid if you always use non-null values (i.e. it allows you to write cleaner code than if you have to check for null everywhere).

Also, you may not have noticed, but the same is true for child relationships; they are never null, even if no records exist:

for(Account record:[SELECT (SELECT Id FROM ActivityHistories) FROM Account WHERE LastActivityDate = NULL]) {
    System.debug(record.ActivityHistories.size());
}

You'll get a whole bunch of 0 values, instead of NullPointerException errors. Again, the intent is to allow us to free up some of our usual null checks and focus on cleaner code.

You should take advantage of this behavior to reduce code complexity, but always remember that it only extends to fields and relationships on SObject variables, and does not extend to any other standard class or method, or to any user-defined classes (e.g. the code that we developers write).


Thinking about this more, I think what we're observing here is also a result of Namespace, Class, and Variable Name Precedence, which I'll quote the relevant parts here:

The parser first assumes that name1 is a local variable with name2 - nameN as field references.

Followed by:

However, with class variables Apex also uses dot notation to reference member variables. Those member variables might refer to other class instances, or they might refer to an sObject which has its own dot notation rules to refer to field names (possibly navigating foreign keys).

Once you enter an sObject field in the expression, the remainder of the expression stays within the sObject domain, that is, sObject fields cannot refer back to Apex expressions.

So, by way of theory:

The system evaluates tc as a local variable, and further names as field references (i.e. you enter the "sObject domain"), and from there, it continues to evaluate the remainder as if tc were originally an sObject itself. This feels like a bug to me, especially since it only works "some of the time." You might want to read more about how the parser works for more information.

... Now I think I need to ask a PM if that's intentional, or if there's a Known Issue regarding the behavior.

| improve this answer | |
  • After reading your answer, I realized that I have actually taken this behavior for granted in Visualforce, e.g. <apex:outputText value="record.Account.Name" />. – IllusiveBrian Aug 5 '16 at 22:56
  • @IllusiveBrian Visualforce is another runtime on top of Apex, and it'll even do more things for you, like assuming "false" when a Boolean is actually null, and so on. But yes, you should always take advantage of language features to reduce code complexity. – sfdcfox Aug 5 '16 at 23:00
  • 1
    But this isn't a name pointing field. It's an object, not an sObject. – Adrian Larson Aug 6 '16 at 0:21
  • @AdrianLarson I think the syntax is tricking the system into going in to the "sObject domain", where the first half of this now expanded answer lies. As long as you can trick the system into believing it should behave differently, it does. No theory on why Id doesn't work, but I did replicate the experience in a unit test I wrote up. It definitely feels disconcerting. – sfdcfox Aug 6 '16 at 0:38
  • Seems similar to salesforce.stackexchange.com/q/134318/2995 – Adrian Larson Aug 6 '16 at 0:42
0

Attempts to reference an Id field value generates the exception because the Contact record was never saved; therefore the Id field is null. All other fields are available, albeit with null values.

I ran into this same issue building a Billing App for Salesforce. The monthly Invoice creation process is based on an "all-or-nothing" approach to writing its Invoice and Transaction records. This means creating all new records in memory and writing them out at the end of a successful run.

Using a custom object for Invoice records, the App first tries to read an existing Invoice record for appending transactions. It may find an Invoice record with only Late Fee transactions, created by another process, and should append to this one.

However, not finding one means creating an unsaved record in memory. At one point I had attempted to reference the Id field on the record in memory and got the 'null pointer' exception.

Try adding a 'save' before referencing the Id field and your exception will disappear.

| improve this answer | |
  • 3
    What you're saying is true, but that's not the issue here. This question's looking at some interesting behavior around traversing relationships in a way that one would expect to throw a NullPointerException but don't, not about building an sObject network through DML. – David Reed Dec 6 '18 at 16:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.