The short version: the safe navigation operator ?. stops a null pointer exception on the way to a value, and the null coalescing operator ?? supplies a replacement once you have one. Between them they remove most defensive if statements. What they do not fix is the two things that actually cause null bugs in Apex: Map.get() returning null for a key that was never there, and a field that was not in the SELECT clause, which throws rather than returning null no matter how many question marks you add.
Safe navigation
?. evaluates the left side, and if it is null the whole expression is null. The right side is never touched.
// Before
String ownerName;
if (opp != null && opp.Account != null && opp.Account.Owner != null) {
ownerName = opp.Account.Owner.Name;
}
// After
String ownerName = opp?.Account?.Owner?.Name;
It works on method calls too, including on collections, which is handy for chained lookups.
Integer count = accountsByRegion?.get('EMEA')?.size();
Two things to hold on to. The result type must be nullable, so Integer count is fine and a primitive arithmetic expression built from it is not. And the right side is skipped entirely, so if the method you chained had a side effect, that side effect does not happen. Do not put work you depend on behind a ?..
Null coalescing
?? returns the left operand unless it is null, in which case it returns the right.
String region = acc.BillingCountry ?? 'Unknown';
Decimal amount = opp.Amount ?? 0;
Integer limitSize = requestedSize ?? DEFAULT_SIZE;
It chains, and it pairs naturally with safe navigation:
String ownerName = opp?.Account?.Owner?.Name ?? 'Unassigned';
That one line replaces a nested if statement and a variable declaration, and it reads in the order you would say it aloud.
The right operand is only evaluated when the left is null, so putting a moderately expensive fallback there is not a problem. Putting a query there still is, because in a loop it runs on every null.
Map.get is the real source of nulls
Map.get(key) returns null for a key that was never added and for a key that was explicitly mapped to null. The two cases are indistinguishable from the return value alone.
Map<Id, Account> accounts = new Map<Id, Account>([
SELECT Id, Industry FROM Account WHERE Id IN :ids
]);
for (Contact c : contacts) {
Account a = accounts.get(c.AccountId);
if (a == null) { continue; } // not returned by the query
c.Industry__c = a.Industry;
}
The null here means something specific: the account was filtered out by sharing, or deleted, or the contact has no account. Skipping silently may be right, or it may be the bug. Decide, and say which in a comment.
When null is a legitimate stored value, containsKey() is the only way to tell the two apart.
if (overrides.containsKey(code)) {
// an override exists, possibly deliberately null
apply(overrides.get(code));
}
Calling containsKey and then get is two hash lookups. That is fine. Do it when the distinction matters and use get with a null check when it does not.
The one that question marks cannot save
Reading a field that was not in the query throws SObjectException with the message about a row retrieved via SOQL without querying the requested field. It does not return null, and ?. does not help, because the object itself is not null.
Account a = [SELECT Id, Name FROM Account LIMIT 1];
String industry = a?.Industry; // still throws
The fix is to query the field. If the field list genuinely varies, check with getPopulatedFieldsAsMap() before reading.
Map<String, Object> populated = a.getPopulatedFieldsAsMap();
String industry = (String) populated.get('Industry');
This is also why a selector with one field list per use case beats passing records around and hoping.
Defensive defaults
The cheapest null safety is not creating nulls.
- Initialise collections at declaration.
private List<String> errors = new List<String>();means nothing downstream needs a check. - Return an empty collection, never null. A method returning null for “nothing found” forces every caller to guard, and one of them will forget.
- Watch Boolean checkbox fields. Checkboxes are never null on a saved record, but a Boolean variable, a formula result or a deserialised value can be, and
if (flag)on a null Boolean throws. - Watch arithmetic. Null in a numeric expression throws rather than treating itself as zero, which is why
?? 0before the sum is the habit to build. - Use
String.isBlankrather than== nullfor text, since it covers empty and whitespace as well.
The exception these prevent is the most common one in Apex, and its five usual sources are in attempt to de-reference a null object.
Frequently asked
Does ?. work on a List index? Not on the bracket form. An out of range index throws ListException rather than returning null, so check isEmpty() or the size first.
Can I use ?. on the left of an assignment? No. It is for reading. Guard the assignment with an if statement.
Is ?? the same as a ternary? Close, but it evaluates the left operand once, where x != null ? x : y writes x twice. With a method call on the left that matters.
Do these work in SOQL or formulas? No. Both are Apex operators. SOQL has != null and formulas have their own null handling functions.
