Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

The quick version: something immediately to the left of a dot, a bracket, or a method call is null. Go to the class and line number in the stack trace and list every reference on that line that could be null — one of them is. The fix is either assigning the thing before you touch it (a List or Map declared but never instantiated is the most common case by a wide margin) or guarding the access with the safe navigation operator ?..

The message names the line, never the variable

Apex throws this the moment you ask a null reference to do something — read a field, call a method, add to a collection, take an index. The full text is short and unhelpful:

System.NullPointerException: Attempt to de-reference a null object

The stack trace gives you class, method, and line. It does not say which reference on that line was null, and it never quotes the expression. On a line like opp.Account.Owner.Email there are three candidates before you consider whether opp itself is null. That is the whole difficulty. The rest is pattern recognition. Flow throws the identical sentence for an entirely different reason and diagnoses nothing like this one — that is Attempt to de-reference a null object, in Flow.

Cause 1: a collection declared but never assigned

Declaring a variable and initialising it are separate steps, and a declared-only collection is null, not empty. This is the highest-frequency source, because the failing line looks completely ordinary.

List<Account> accounts;          // null, not an empty list
accounts.add(new Account());     // throws here

Map<Id, Contact> contactsById;   // same problem
contactsById.put(c.Id, c);       // and here

Assign on the declaration line and the failure mode disappears. The nastier variant is a class member initialised inside a constructor’s if branch — instance fields default to null with no warning, so initialise member collections inline on the field declaration instead. There is no branch to miss.

Cause 2: Map.get() returns null for a key that isn’t there

A Map returns null for a missing key. It does not throw and it does not return an empty value, so the exception lands one line later on whatever you did with the result.

Map<Id, Account> accountsById = new Map<Id, Account>([
    SELECT Id, Name, Industry FROM Account WHERE Industry = 'Technology'
]);

for (Contact c : contacts) {
    String industry = accountsById.get(c.AccountId).Industry;   // throws
}

Two different rows fail for two different reasons. A Contact with no account has a null AccountId, so the key is null. A Contact whose account was filtered out by the WHERE clause has a key that is simply absent from the Map. Both return null.

for (Contact c : contacts) {
    Account a = accountsById.get(c.AccountId);
    if (a == null) { continue; }
    String industry = a.Industry;
}

Assign the lookup to a local before using it even when you are confident the key exists. It gives you somewhere to put the guard.

Cause 3: traversing a relationship that points at nothing

Parent traversal mirrors the data. If AccountId is null on an Opportunity then opp.Account is null, and opp.Account.Name throws.

for (Opportunity opp : [SELECT Id, AccountId, Account.Name FROM Opportunity]) {
    String name = opp.Account?.Name ?? 'No account';
}

The safe navigation operator “short-circuits expressions that attempt to operate on a null value and returns null instead of throwing a NullPointerException”, per the Apex Developer Guide. Chain it at every hop that can be null, opp.Account?.Owner?.Email, because guarding only the first hop leaves the second one live.

One caution: ?. converts a loud failure into a silent null. That is right for display logic and wrong for a calculation whose result feeds a DML update. If a null parent means the record is misconfigured, you want the exception, or an addError, not a null propagating into a field value.

Cause 4: a method that returns null and a caller that never checks

public static Account getPrimaryAccount(Id contactId) {
    List<Account> results = [SELECT Id, Name FROM Account WHERE Id IN (
        SELECT AccountId FROM Contact WHERE Id = :contactId
    )];
    return results.isEmpty() ? null : results[0];
}

String name = getPrimaryAccount(c.Id).Name;   // throws whenever there is no match

The author was careful: the isEmpty() check is right there, and pushed the problem onto every caller instead. Returning null from a method that says nothing about it is the most reliably misused signature in Apex.

Either return an empty List, so a caller who forgets to check gets a loop that does nothing rather than a production exception, or throw a typed exception when a missing record is genuinely an error. I default to the empty list, because the failure mode is quiet in the right way.

Cause 5: Trigger.oldMap is null on insert

A handler that compares old and new values works on update and dies on insert. Both Trigger.old and Trigger.oldMap are documented as “only available in update and delete triggers”. In an insert context they are null, not empty.

for (Account a : Trigger.new) {
    Account old = Trigger.oldMap.get(a.Id);   // throws on insert
    if (old.Rating != a.Rating) { /* ... */ }
}

Wrapping it in if (Trigger.isUpdate) fixes the symptom. The structural fix is to route each context to its own handler method, so a change-detection routine cannot be called with no prior state to compare against. Give it the signature handleAfterUpdate(List<Account> records, Map<Id, Account> oldMap) and the insert path cannot call it by accident.

Finding the null in under two minutes

  • Count the dots on the line the trace names. Every dot is a dereference and a suspect. Split a chained expression across several lines and re-run. The trace then points at the exact hop.
  • Scan for declarations with no new on the same line. This finds cause 1 in seconds.
  • Log the suspects, not the result. System.debug('id=' + c.AccountId + ' acct=' + accountsById.get(c.AccountId)); tells you which one is null. Logging the expression that throws tells you nothing, because it throws first.
  • If only some rows fail, the cause is data. Query the failing Id and look at its foreign keys.
  • Confirm it is actually this exception. Reading a field you left out of the SELECT list raises System.SObjectException: SObject row was retrieved via SOQL without querying the requested field. a different exception with a different fix.

Patterns that stop it recurring

Initialise every collection on its declaration line, treat Map.get() as nullable and assign it to a local, and never write a method that returns a single sObject or null without saying so in its name. Use ?. on display paths and explicit checks where a null means something is wrong.

One test convention catches most of the rest. For anything trigger-adjacent, write an insert test as well as an update test, using records whose optional lookups are deliberately empty. An Opportunity with no Account, a Case with no Contact. Those records find more null dereferences than any amount of code review.

Reproduced in a Summer ’26 scratch org on 14 September 2026: calling .length() on a null String throws System.NullPointerException: Attempt to de-reference a null object, with no indication of which variable was null. That omission is why the five sources below are worth knowing by sight.

Frequently asked

Why does an empty list not throw when I loop over it?

An empty list is an object. A null list is not. for (Account a : new List<Account>()) runs zero iterations, for (Account a : someNullList) throws. That distinction is why initialising on declaration removes a whole class of bug.

Does the safe navigation operator work on Map.get(), and list indexes?

It works on the result of a method call, so accountsById.get(c.AccountId)?.Industry returns null for a missing key. It does not save you from a null Map itself, and it cannot rescue an out-of-bounds index, which raises a ListException.

Can I just wrap it in try-catch?

You can, and you almost never should. A null dereference means your code assumed something the data does not support. Catching it hides the assumption and usually leaves a partial update behind.

Why does it happen in production but not in my test?

Test methods on API version 24.0 and later cannot see pre-existing org data, so your test runs on records you built with every field populated. Production has the Opportunity with no Account. Build one deliberately sparse record per test class.

Is a null field value the same as a null object?

No. a.Industry == null is a legal read of an empty field. a.Industry where a is null is the dereference that throws. The exception is always about the reference left of the dot, never the field’s value.


Reference: Safe Navigation Operator and Trigger Context Variables, Salesforce Apex Developer Guide.