Learn MOAR with Winter ’21: Safe Navigation in Apex

Introduction

Let’s take a quick tour of what’s new for Apex in Winter ’21: We’ll look at two major highlights, starting with a brand new language operator that makes it substantially easier to work with null values. Then, we’ll take a look at new sObject error management methods that enable writing true unit tests around DML error logic. Don’t forget to follow up with a peek at the release notes for plenty of other Apex features and improvements that are part of this release.

Tune into Release Readiness Live on Friday September 18 at 9:00 a.m. PT to hear more about these features, as well as live demos. Register here!

Safe navigation operator

Replace boring, sequential checks for null references in code with the new safe navigation operator (?.). In its most basic form you can think of this operator as shorthand for “if the left side is null, return null instead of causing a NullPointerException”. You can use this operator to dramatically cut down on the number of times you write something like if(var != null) { var.doStuff(); }, and instead use the short-cut var?.doStuff();. Both of these expressions do the exact same thing, but one of them involves much more typing — and the effort imbalance increases when you start looking at nested properties. Nobody wants to write the long-hand form of foo?.bar?.baz?.doWork(); if they don’t have to!

At a more technical level, this means you can effectively short-circuit the evaluation of the right-hand side of a chain expression, which may include variable references, method references or array expressions, if the left-hand side evaluates to null.

An important use of the safe navigation operator that’s unique to Apex is in eliminating null field checks in SOQL queries like this:

// Existing lengthy code checking for null fields
results = [SELECT Name FROM Account WHERE Id = :accId];
if (results.size() == 0) { // Account doesn't exist, isn't visible, was deleted, etc
    return null;
}
return results[0].Name;

// New crisp code using the safe navigation operator
return [SELECT Name FROM Account WHERE Id = :accId]?.Name;

Let’s look at simplifying existing code with this example that interacts with Apex types stored in platform cache. In our existing codebase we have an Apex class, Character, that represents the different attributes about our Trailhead mascot friends. In that class, we have a method that looks up character data from platform cache where their information is already loaded. For the sake of brevity, we don’t include code that handles cache misses in this example, but you must always handle that in a real application!

Here’s what our existing code looks like:

public with sharing class Character{
    private static final String CHARACTER_DETAILS_PARTITION = 'CharacterDetails';

    public static List<String> getHobbiesFromCache(String name){
        Cache.Partition part = Cache.Org.getPartition(CHARACTER_DETAILS_PARTITION));

        if(part == null){
            return null;
        }
        CharacterDetails data = (CharacterDetails)part.get(name);
        if(data == null){
            return null;
        }else{
            return data.getHobbies();
        }
    }
}

With repeated use of the safe navigation operator, we can reduce the getHobbiesFromCache() method to a single line:

return (CharacterDetails)Cache.Org.getPartition(CHARACTER_DETAILS_PARTITION)?.get(name))?.getHobbies();

Wow, that’s really quite a line! In hindsight, perhaps we’ve used the safe navigation operator excessively; with all the casts and nested parentheses, it is a bit too hard to read. Turns out there can be too much of a good thing, so let’s compromise for the sake of readability:

CharacterDetails namedCharacter = (CharacterDetails)Cache.Org.getPartition(CHARACTER_DETAILS_PARTITION)?.get(name));
return namedCharacter?.getHobbies();

And now we have something that’s easy to read. In our first statement, we handle safe navigation through looking up the partition and storing our results as the expected Apex type of the result. Next, we use safe navigation again to make sure that even if we got back a null instead of a character, our call to getHobbies() doesn’t throw an exception.

Learn more from the Apex Developer Guide here. And of course we have release notes – read them here!

sObject error methods

We added new methods to the sObject class (and, of course, thanks to inheritance, all of its subclasses (like Account, Contact, and so on) that greatly simplify adding, checking and validating error messages associated with sObject fields.

There are really three new sObject methods to keep in mind here: addError(), getErrors() and haveErrors(). In the case of addError(), there are a number of overloads for convenience.

The most notable, the SObject.addError() method, is in response to a request from the IdeaExchange. The method lets you specify the sObject field at runtime. Previously, to add an error to a field in Apex, you had to know the field’s name at compile time. This could be a major upgrade, especially for AppExchange applications where you might not know about customizations at the time you write your code.

To pair with this extra love for working with sObject save errors, we added a new SObject.getErrors() method. This method allows you to obtain errors without performing any DML operation. This is an outstanding feature for writing true unit tests that never touch the database. As with any get() method that returns a list, you may not care about the actual list contents, and just want to know if there are any errors. For this purpose, we have the (slightly) faster SObject.hasErrors() method.

Here is a pared-down example of validating that errors were added to an sObject successfully, without needing to actually do any DML operations:

// Add an error to a field of an SObject instance using the addError() method.
String fieldToValidate = /** let's assume we looked this value up from custom metadata **/;
Account acct = new Account(name = 'TestAccount');
Schema.SObjectField nameDesc = 
    Schema.SObjectType.Account.fields.getMap().get(fieldToValidate);
Schema.sObjectField nameField = nameDesc.getSObjectField();
acct.addError(nameField, 'error is name field');

// Use the hasErrors() method to verify that the error is added
System.Assert(acct.hasErrors());

// Use the getErrors() method to validate the error.
List<Database.Error> errors = acct.getErrors();
System.assertEquals(1, errors.size());
System.assertEquals(fieldToValidate, errors[0].getFields()[0], 
    'Expected validation error on configured field '+fieldToValidate);

All the new methods are documented in the Apex Developer Guide here.

Conclusion

There’s more than just these changes coming. Take a deeper dive into the release notes to see what else has changed, from Apex callouts supporting the PATCH verb, to DML-free platform event firing.

Everything we’re talking about here is generally available in Winter ’21. No pilot signup, no waiting, no catch. Go explore!

Share what you love about the Winter ’21 Release with the #LearnMOAR hashtag. Also, don’t forget to sign up for Release Readiness Live happening on September 18, 2020.

About the authors

Gita Krishnan is a Staff Technical Writer for Apex. You may know her work from the Apex Developer Guide, and just about everything Apex-related in the Release Notes.
gita.jpeg

Chris Peterson is a Director of Product Management for Apex, who really hopes he made the right prioritization decisions here and welcomes feedback on twitter.