Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Apex

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

You tried to read a field, call a method, or index into something that turned out to be null. Apex doesn't have safe-navigation by default, so any `obj.field` blows up the moment `obj` is null.

Also seen asAttempt to de-reference a null object·System.NullPointerException·FATAL_ERROR System.NullPointerException

A Lightning page that has worked for months suddenly breaks for one user. The page is backed by an Apex controller that loads an Opportunity, walks through related contacts, and returns a summary. The user sees a red error banner reading System.NullPointerException: Attempt to de-reference a null object. Other users on the same page work fine. The Opportunity record looks unremarkable. The developer needs to find which dereference is the culprit.

What the platform is checking

Every reference type in Apex (SObject, String, Date, custom class, List, Map, Set) can hold a null value. When code asks a null value for a field, a method, or an element, the Apex runtime throws NullPointerException: Attempt to de-reference a null object. The exception is thrown at the exact statement that tried to read through the null, not at the statement that originally produced the null.

The cause is always the same shape. A variable is expected to point at an object. It points at nothing. The line that touches the variable expecting an object finds nothing there and refuses to continue. Apex returns the exception with a stack trace, leaving the transaction rolled back if any DML had executed.

The challenge is rarely the exception itself. The challenge is finding the variable. The error message names no specific field, no specific line in your own code. The stack trace identifies the line where the deref happened, but the producer of the null could be far upstream, possibly in code you didn't write.

The broken example

A controller that summarizes an Opportunity and its primary Contact:

public class OpportunitySummaryController {
    @AuraEnabled
    public static String buildSummary(Id opportunityId) {
        Opportunity opp = [
            SELECT Id, Name, Amount, Account.Name, Account.Owner.Email
            FROM Opportunity
            WHERE Id = :opportunityId
            LIMIT 1
        ];
        OpportunityContactRole primary = [
            SELECT Contact.Email, Contact.Title
            FROM OpportunityContactRole
            WHERE OpportunityId = :opportunityId AND IsPrimary = true
            LIMIT 1
        ];
        return opp.Name + ' for ' + opp.Account.Name +
            ' (owner ' + opp.Account.Owner.Email + ', primary ' + primary.Contact.Email + ')';
    }
}

Three null hazards lurk here.

First, the Opportunity might have no Account. AccountId is not required on Opportunity by default. opp.Account would be null and opp.Account.Name would throw.

Second, the Account's Owner could be inactive or anonymized. The Owner relationship returns null when the user record is unavailable to the running context, and opp.Account.Owner.Email would throw.

Third, an Opportunity might have no primary Contact Role. The SOQL query returns zero rows, which becomes a QueryException rather than a null, but a related fix that uses [SELECT ...].get(0) would produce a null reference.

A second shape that surprises developers: trigger code that assumes Trigger.oldMap.get(record.Id) returns a record. On before insert, Trigger.oldMap is null because there is no prior state. On after delete, Trigger.newMap is null. Asking for a key on a null map throws this exception.

trigger CaseTrigger on Case (before insert, before update) {
    for (Case c : Trigger.new) {
        Case oldRec = Trigger.oldMap.get(c.Id); // throws on insert
        if (oldRec.Status != c.Status) {
            // ...
        }
    }
}

A third shape: collection methods that return null when nothing matches. Map.get(key) returns null for a missing key. String.split(...) can return a List with a null element if the input is null. JSON.deserialize(...) can return null for missing JSON properties.

The fix, three paths

Check for null before dereferencing. The simplest fix is a guard at every site where a value could be null.

String accountName = opp.Account != null ? opp.Account.Name : 'No Account';
String ownerEmail = opp.Account != null && opp.Account.Owner != null ? opp.Account.Owner.Email : 'Unknown';

The guard is verbose. For a chain of three references, you write three checks. The verbosity is a tax that catches every null without surprises.

Use the safe navigation operator. Apex supports ?. which short-circuits the chain when any link is null.

String ownerEmail = opp?.Account?.Owner?.Email;

When opp or opp.Account or opp.Account.Owner is null, the expression evaluates to null without throwing. The operator was introduced in API version 52.0 and removes most of the boilerplate.

Make the input shape guarantee non-null. When a method documents that its inputs must be non-null, callers carry the responsibility. The method itself can validate at entry and fail fast with a clearer message.

public static String buildSummary(Opportunity opp) {
    if (opp == null) {
        throw new IllegalArgumentException('opp is required');
    }
    if (opp.AccountId == null) {
        return opp.Name + ' (no account)';
    }
    // proceed
}

The contract is now explicit. A caller that passes null gets a clear exception with a useful message instead of a deep stack trace.

The fixed example

A revised controller using safe navigation and explicit checks:

public class OpportunitySummaryController {
    @AuraEnabled
    public static String buildSummary(Id opportunityId) {
        if (opportunityId == null) {
            throw new AuraHandledException('Missing opportunityId');
        }
        List<Opportunity> opps = [
            SELECT Id, Name, Amount, Account.Name, Account.Owner.Email
            FROM Opportunity
            WHERE Id = :opportunityId
            LIMIT 1
        ];
        if (opps.isEmpty()) {
            throw new AuraHandledException('Opportunity not found');
        }
        Opportunity opp = opps[0];

        List<OpportunityContactRole> roles = [
            SELECT Contact.Email, Contact.Title
            FROM OpportunityContactRole
            WHERE OpportunityId = :opportunityId AND IsPrimary = true
            LIMIT 1
        ];
        OpportunityContactRole primary = roles.isEmpty() ? null : roles[0];

        String accountName = opp?.Account?.Name ?? 'No account';
        String ownerEmail = opp?.Account?.Owner?.Email ?? 'No owner email';
        String contactEmail = primary?.Contact?.Email ?? 'No primary contact';

        return opp.Name + ' for ' + accountName +
            ' (owner ' + ownerEmail + ', primary ' + contactEmail + ')';
    }
}

The method validates the input, handles the empty-result case, and uses safe navigation with the null-coalescing operator ?? for default values. Every dereference either short-circuits cleanly or produces a placeholder string the UI can display.

Edge case: SObject default values versus null

Numeric and boolean fields on SObjects are Apex Decimal, Integer, and Boolean types. They can hold null even when the underlying field stores zero or false in the database. A field marked "required" at the field level can still be null in an Apex SObject if you constructed the SObject manually with new Case() and didn't set the field.

The defensive pattern: read fields as their wrapper types and check for null before arithmetic.

Decimal amount = opp.Amount ?? 0;
Decimal probability = opp.Probability ?? 0;
Decimal weighted = amount * (probability / 100);

Skipping the null check causes a NullPointerException at the multiplication.

Edge case: maps and missing keys

Map.get(key) returns null for any key not in the map. The following pattern is common and broken:

Map<Id, Account> accountsById = new Map<Id, Account>([SELECT Id, Name FROM Account WHERE Id IN :accountIds]);
for (Id accountId : someOtherList) {
    String name = accountsById.get(accountId).Name; // throws if accountId not in map
}

The fix is to check or to populate the map differently.

Account a = accountsById.get(accountId);
if (a != null) {
    String name = a.Name;
}

The same applies to containsKey versus get. Use containsKey when you want to know whether the key exists, and get when you want the value (and are prepared for null).

Edge case: Apex limits and aggregate queries

AggregateResult rows from SUM, AVG, COUNT return null when the underlying records contain no rows or when the field being aggregated is itself null.

AggregateResult ar = [SELECT SUM(Amount) total FROM Opportunity WHERE AccountId = :accountId];
Decimal total = (Decimal) ar.get('total'); // null if no opportunities

Cast to a wrapper type and check before arithmetic. A bare Decimal total = (Decimal) ar.get('total'); return total * 1.1; throws if no Opportunities exist.

Test patterns

A test that exercises every null path the production code might hit:

@IsTest
static void summaryHandlesMissingAccount() {
    Opportunity opp = new Opportunity(
        Name = 'Test Opp',
        CloseDate = Date.today(),
        StageName = 'Prospecting',
        Amount = 1000
    );
    insert opp;
    Test.startTest();
    String result = OpportunitySummaryController.buildSummary(opp.Id);
    Test.stopTest();
    System.assert(result.contains('No account'), 'Should fall back gracefully');
}

@IsTest
static void summaryThrowsForUnknownId() {
    Test.startTest();
    try {
        OpportunitySummaryController.buildSummary('006xx000000000A');
        System.assert(false, 'Expected AuraHandledException');
    } catch (AuraHandledException e) {
        System.assert(e.getMessage().contains('not found'));
    }
    Test.stopTest();
}

Each test covers a distinct null hazard. Adding more cases for null Owner, missing Primary Contact Role, and bad input completes the matrix.

Diagnosing in production

When the exception fires:

  1. Read the stack trace and find the line in your code (skip framework frames).
  2. Identify the expression being dereferenced. Is it a SOQL relationship? A map lookup? A method return?
  3. Hypothesize which step produced the null. Often a query returned no rows, a relationship lookup walked off the end of the data, or a parent field was missing.
  4. Reproduce with the exact data shape. Take the record id from the error log, query it with the same fields, and inspect the JSON.
  5. Add a guard at the deref site, return a sensible default, or fail fast with a clearer message.

A debug-log pass with strategic System.debug statements catches the producer when the line of code is ambiguous.

Anti-pattern: blanket try-catch around everything

A reflex response is wrapping the whole method in try/catch(Exception) and logging. This hides the bug. The Lightning page shows a generic message, the cause is buried in logs, and the underlying null still produces incorrect business behavior. The right fix is to identify the specific null, guard the specific path, and let unexpected exceptions still surface.

Anti-pattern: assigning defaults inside the query loop

Some code attempts to fix nulls inside the loop that consumes the records.

for (Opportunity o : opps) {
    if (o.Amount == null) o.Amount = 0;
    // ...
}

The assignment changes the in-memory record but does not persist anything to the database. The next time another piece of code queries the same record, the field is still null. Worse, if the local code passes the modified record to a downstream service that expects database-faithful values, the service receives surprise mutations.

The correct pattern is to derive a local variable for the calculation and leave the SObject unchanged.

for (Opportunity o : opps) {
    Decimal amount = o.Amount ?? 0;
    // use amount for the calculation
}

The SObject keeps its original shape. The calculation has the value it needs.

Defensive habits

Treat every SObject relationship as nullable until proven otherwise. opp.Account.Name should be guarded unless you have a validation rule that forbids Opportunities without Accounts. Even then, parent records can be archived or deleted, and the relationship in cache can lag.

Use the safe navigation operator for read-only chains and explicit checks for assignment paths. opp?.Account?.Name reads cleanly. account.Owner = newOwner needs an explicit check that account is non-null because the safe operator on the left side of an assignment is not allowed.

Test with realistic data. The default test fixture often populates every required field and every relationship. Add tests that deliberately omit optional fields so the null branches get exercised.

Adopt a linting rule that flags [SELECT ...] followed by direct field access without a check for empty results. SOQL queries returning zero rows are the most common source of subsequent null references.

Beyond the safe navigation operator

The ?. operator covers most cases, but it interacts in surprising ways with method calls and arithmetic. A few patterns are worth knowing.

Chained method calls on collections follow the operator from the left. accounts?.size() returns null if accounts is null, zero otherwise. The conditional reads cleanly and avoids the explicit null check.

Arithmetic with potentially null wrapper types still requires explicit guarding. The expression opp?.Amount * 1.05 does not work; multiplying null by anything throws. Convert to a default first.

Decimal safeAmount = opp?.Amount ?? 0;
Decimal projected = safeAmount * 1.05;

The null-coalescing operator ?? was added in API 60.0. For older API versions, the ternary form opp != null && opp.Amount != null ? opp.Amount : 0 is the equivalent. Reviewing your project's sourceApiVersion clarifies which form to use.

Logging and observability for null incidents

A single line of logging per defensive guard can save significant debugging time when the next incident fires.

if (opp?.Account == null) {
    System.debug(LoggingLevel.WARN, 'Missing Account on Opportunity ' + opp.Id);
    return 'No account';
}

The log entry confirms the path was taken and identifies the record id. Production logs for AuraEnabled methods are limited in retention, so persisting these warnings to a custom Log__c object or to an external monitoring tool gives longer visibility.

For Lightning components that surface AuraHandledException to the UI, include the record id in the user-visible message when safe. "Could not load summary for record 006xx000004CFkA" is more actionable than "Could not load summary".

Quick recovery checklist

  1. Identify the dereference that threw, from the stack trace.
  2. Hypothesize which value is null.
  3. Reproduce in a sandbox using the failing record's id.
  4. Add a guard, return a default, or fail fast with a useful message.
  5. Add a regression test that covers the missing field.
  6. Deploy and confirm.

Most NullPointerException incidents resolve in under an hour once the producer is found. The rare ones involve deep object graphs where two layers down a relationship is missing, and those benefit from a structured review of the data model's nullability contract.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Apex errors