Look at the failing line and count the dots. Every . is a dereference and any one of them can be the culprit. In practice it is a relationship traversal through an empty lookup (c.Account.Name with no AccountId), a Map.get() that missed, or a collection that was declared but never given a new. Safe navigation (?.) silences it in one character — decide first whether null is legitimate data or a bug upstream.
What "de-reference" means here
A variable of a non-primitive type holds a reference to an object, or it holds null. Dereferencing is following that reference to get at the thing — reading a field, invoking a method, indexing a list. Apex performs no implicit check before doing it, so following a null reference throws System.NullPointerException with the fixed message Attempt to de-reference a null object.
Three details matter for triage. The message is always identical — it never names the variable, so it carries no diagnostic information beyond the stack line. It fires on method calls as well as field reads, so a null String and a call to .trim() on it produce this, not a "no such method" error. And it is a runtime error: it compiles fine, deploys fine, and only appears when the data hits the null path.
Not everything null-adjacent produces it. Comparing with == against null is safe, string concatenation with + renders a null operand as the text null rather than throwing, and a null map value returned from get() is only a problem once you dereference it.
The six causes, ranked
1. Relationship traversal through a null lookup
The most common cause in real code, and the one that only bites on some records. A populated lookup gives you a parent object to traverse; an empty lookup gives you null, and the second dot throws:
List<Contact> contacts = [SELECT Id, AccountId, Account.Name FROM Contact];
for (Contact c : contacts) {
// Throws for every Contact with no AccountId
System.debug(c.Account.Name);
}
// Guard the lookup, not the parent field
for (Contact c : contacts) {
if (c.AccountId != null) {
System.debug(c.Account.Name);
}
}
// Or use safe navigation — evaluates to null instead of throwing
for (Contact c : contacts) {
System.debug(c.Account?.Name);
}Checking c.AccountId != null is stricter than checking c.Account != null: it also fails fast when the parent was never queried at all, which is a different bug wearing the same costume.
2. A Map.get() that missed
Map.get() returns null for an absent key — it does not throw and it does not warn. Chaining straight off the result is the classic trigger bug, especially against Trigger.oldMap on insert, where the map is null to begin with:
Map<Id, Account> accountsById = new Map<Id, Account>(
[SELECT Id, Name, OwnerId FROM Account WHERE Industry = 'Technology']
);
for (Opportunity o : Trigger.new) {
// Throws for any Opportunity whose Account is not in the filtered map
o.OwnerId = accountsById.get(o.AccountId).OwnerId;
}
// Resolve first, then guard
for (Opportunity o : Trigger.new) {
Account parent = accountsById.get(o.AccountId);
if (parent != null) {
o.OwnerId = parent.OwnerId;
}
}Two adjacent traps. A WHERE clause on the query that built the map means the map is deliberately incomplete — a miss is expected data, not corruption. And Trigger.oldMap is null in insert contexts, so a handler shared across events needs Trigger.isUpdate gating before it touches it.
3. A collection or member variable declared but never initialised
Declaring a List, Set, or Map does not create one. An uninitialised collection field is null, and the first add() throws. This hides well in class fields, where the declaration and the use are hundreds of lines apart:
public class OrderCollector {
// Null until something assigns it — every add() below throws
private List<Order> pending;
public void collect(Order o) {
pending.add(o);
}
}
// Initialise at the declaration. There is no reason to leave it null.
public class OrderCollector {
private List<Order> pending = new List<Order>();
public void collect(Order o) {
pending.add(o);
}
}4. A helper that returns null on its not-found path
A service method that returns null when it finds nothing pushes the failure to every caller, and the callers forget. The fix is at the producer, not the consumer: return an empty collection for list-shaped results, and make single-record lookups explicitly optional in their naming so the null is visible at the call site.
// Every caller must remember to null-check
public static List<Account> findByIndustry(String industry) {
if (String.isBlank(industry)) return null;
return [SELECT Id, Name FROM Account WHERE Industry = :industry];
}
// Empty is a valid answer; null is not
public static List<Account> findByIndustry(String industry) {
if (String.isBlank(industry)) return new List<Account>();
return [SELECT Id, Name FROM Account WHERE Industry = :industry];
}Note that querying into a single SObject variable when there are no rows is a different error — System.QueryException: List has no rows for assignment to SObject. If you see that one instead, the query returned nothing rather than something null.
5. Null elements inside a collection, or unassigned wrapper fields
A list can contain nulls. A loop that builds one conditionally, an Apexpages or JSON deserialisation that leaves gaps, or a wrapper class whose inner SObject field was never populated all produce a collection that iterates fine and throws on the element access. JSON.deserialize in particular leaves any field absent from the payload at null, including nested objects.
6. Static state assumed to survive into an async context
A @future method, a Queueable, a Batch, and a scheduled job each run in their own transaction with fresh statics. Code that lazily initialises a static map in the synchronous transaction and reads it in the async one finds null. Initialise inside the async entry point, or pass the state in through the constructor.
Safe navigation, and when not to use it
The ?. operator short-circuits the whole chain to null when the left side is null, and it chains:
// Any null link yields null overall — no exception
String ownerName = opp?.Account?.Owner?.Name;
// Works on method calls too
Integer len = someString?.trim()?.length();
// Combine with the null-coalescing pattern for a default
String display = (opp?.Account?.Name == null) ? 'No account' : opp.Account.Name;Use it where null is a legitimate state of the data. Do not use it to paper over a value that should never be null — swapping a loud exception for a silently null result moves the failure downstream, where it becomes a wrong number in a roll-up instead of a stack trace with a line number. If null means "something upstream is broken", assert it and let it throw.
Finding which dereference was null
The stack trace gives you a class and a line, and that line often has four candidate dereferences. Two things narrow it fast:
- Split the line. Break the chain into intermediate local variables. The stack trace now points at exactly one dereference, and the change is trivially reverted once you know the answer.
- Check the query, not just the data. A field that was never selected in the SOQL behaves differently from a field that is genuinely null on the record — an unqueried field raises
System.SObjectException: SObject row was retrieved via SOQL without querying the requested field, so if you are getting a plain NullPointerException the field really is null in the data.
Reproduce it locally in seconds
Nimbus runs Apex locally against an embedded PostgreSQL, and it raises this exception with the same type and the same message the platform uses — a null field read, a chained call on a null, a miss on Map.get() dereferenced, and iterating a null collection all throw System.NullPointerException: Attempt to de-reference a null object locally. So the bisect-the-chain loop above costs a second per attempt instead of a deploy.
# Run the failing test
nimbus test "ContactServiceTest.testAccountNameRollup"
# Full account of the failure: exception type, file and line,
# and the SOQL/DML executed immediately before it
nimbus explain ContactServiceTest.testAccountNameRollup
# Or probe an expression directly, no test needed
nimbus exec -c "Contact c = new Contact(); System.debug(c.Account.Name);"