Monday, March 6, 2017

[Salesforce / Apex Runtime] Bug Report: Null exception not thrown

This little and simple post is to get Community's attention to a possible bug my colleagues and me found few days ago.

It's about how null checking differs if occurring from outside or inside an Apex Class.

For those who are TL;DR (like me), jump to the following Gist and comment if you have an explanation.

Let's take this simple class:

public class MyController {
    public Account tst{get;set;}
    public MyController(){
        this.tst = [Select Id, Name From Account limit 1];
    }
    
    public void myMethod(){
        system.debug('Test inside: '+(tst.Name == null)); //Throws null pointer exceptions
        system.debug('Test inside: '+(tst == null));
        system.debug('Test inside: '+json.serializepretty(tst));
    }
}

Let's take the following anonymous code:

MyController cnt = new MyController();
cnt.tst.Name = 'test';
cnt.tst = null;
system.debug('Test outside: '+(cnt.tst.Name == null));
system.debug('Test outside: '+(cnt.tst == null));
system.debug('Test outside: '+json.serializepretty(cnt.tst));
cnt.myMethod();

Question: What do you think is the debug?

Answer:

19:06:58.24 (32784451)|USER_DEBUG|[5]|DEBUG|Test outside: true
19:06:58.24 (32903967)|USER_DEBUG|[6]|DEBUG|Test outside: true
19:06:58.24 (33182107)|USER_DEBUG|[7]|DEBUG|Test outside: null
19:06:58.24 (33393615)|EXCEPTION_THROWN|[7]|System.NullPointerException: Attempt to de-reference a null object

Yes, that is!

Apparently calling a nullified member of type Sobject of an object instance from outside is somehow handled by the runtime as null, whether you recall a field (cnt.tst.Name == null) or the member itself (cnt.tst == null); while calling the same member from inside an object method, correctly throws a null pointer exception as expected.

This seems related to how Sobjects are handled when used in SOQL relationship fields, such as:

List<Contact> cntList = [Select Id, Account.Name From Contact limit 1];
System.debug('Account is: '+cntList[0].Account);
System.debug('Account.Name is: '+cntList[0].Account.Name);

In this example if the Account parent object is not presente, the debug outputs:

19:18:17.18 (79345369)|USER_DEBUG|[3]|DEBUG|Account is: null
19:18:17.18 (79634328)|USER_DEBUG|[4]|DEBUG|Account.Name is: null

This is somehow confusing.

I get that when dealing with SOQL queries this is useful (no null checking) but dealing with custom Apex Classes this can lead to real confusion.

These are my 2 cents, what do you think?