How to Fix Null Pointer Exceptions in Apex Collection Workflows

Reading Time: 20 min
Author: William Watson Published: March 14, 2025 Updated: March 13, 2026 System: Trigger and async Apex collection workflows Scale: High-volume record processing and batch operations Failure: Null Pointer Exceptions in map/list/set flows Audience: Salesforce developers and technical leads
Jump to section Current: Top of article
Direct answer

Initialise collections by default, guard lookups before dereferencing, and choose empty-return patterns over null returns so asynchronous and trigger workflows do not fail at runtime.

Why do Apex collection workflows fail with Null Pointer Exceptions?

Most collection-related Null Pointer Exceptions happen because code assumes a Map, List, or lookup result always exists when runtime data says otherwise.

In production, this shows up when one of these conditions appears:

  • A map was declared but never initialised before get() or put().
  • A key lookup returns null, then code immediately dereferences it.
  • A helper method returns null instead of an empty collection.
  • Async jobs process partial data where expected parent rows are missing.

What is the safest default for Apex collection variables?

Use initialise-first defaults and return empty collections, not null.

public with sharing class InvoiceService {
    public static Map<Id, Decimal> loadInvoiceTotals(Set<Id> accountIds) {
        Map<Id, Decimal> totalsByAccount = new Map<Id, Decimal>();

        if (accountIds == null || accountIds.isEmpty()) {
            return totalsByAccount;
        }

        for (AggregateResult row : [
            SELECT Account__c accountId, SUM(Total__c) amount
            FROM Invoice__c
            WHERE Account__c IN :accountIds
            GROUP BY Account__c
        ]) {
            totalsByAccount.put(
                (Id) row.get('accountId'),
                (Decimal) row.get('amount')
            );
        }

        return totalsByAccount;
    }
}

This pattern avoids null checks across every caller and keeps flow predictable.

Decision and Trade-offs

Pattern Benefit Risk / Cost
Return null for “no data” Slightly less memory allocation Every caller must defend against null; easy to miss in async/trigger paths
Return empty collection (recommended) Predictable API contract and fewer runtime failures Minor allocation cost
Null-check only at controller boundary Cleaner service internals Hidden failures when reused from jobs, triggers, and queueables
Defensive checks at every dereference Strong runtime safety More verbose code; needs consistent team standards

For Salesforce workloads, reliability and predictable behavior usually matter more than micro-optimising allocations.

Which code paths are most likely to fail in real orgs?

1) Lookup then dereference

Opportunity opp = oppById.get(oppId);
// Fails when opp is null
if (opp.StageName == 'Closed Won') {
    // ...
}

Safer version:

Opportunity opp = oppById.get(oppId);
if (opp != null && opp.StageName == 'Closed Won') {
    // ...
}

2) Nested map access without intermediate guards

// Fails if accountToContacts.get(accountId) returns null
Contact primary = accountToContacts.get(accountId)[0];

Safer version:

List<Contact> contacts = accountToContacts.get(accountId);
if (contacts != null && !contacts.isEmpty()) {
    Contact primary = contacts[0];
}

3) Uninitialised write target collection

List<Task> followUps;
followUps.add(new Task(Subject = 'Call customer'));

Safer version:

List<Task> followUps = new List<Task>();
followUps.add(new Task(Subject = 'Call customer'));

How should you design helper method contracts?

Define explicit contracts so callers know exactly what to expect:

  • Collection-returning methods return empty collections, never null.
  • Single-record methods may return null, but caller must guard before dereference.
  • Methods that require non-null params should fail fast with meaningful exceptions.
public static List<Case> getOpenCasesByAccount(Id accountId) {
    if (accountId == null) {
        throw new IllegalArgumentException('accountId is required');
    }

    List<Case> rows = [
        SELECT Id, Subject, Status
        FROM Case
        WHERE AccountId = :accountId
        AND IsClosed = false
    ];

    return rows == null ? new List<Case>() : rows;
}

What changes when processing large data volumes?

Null pointer risk increases with scale because data quality variance increases.

At higher volumes:

  • Related records are more likely to be missing in the same transaction.
  • Async chunks can process incomplete data windows.
  • Partial retries can re-enter code paths with different state.

Scale-safe practices:

  • Treat every map lookup as optional unless guaranteed by query design.
  • Prefer guard clauses early in loops.
  • Log key IDs when null conditions are unexpected.
  • Separate data loading from business logic to make assumptions obvious.

Debugging checklist for Null Pointer Exceptions in Apex

When this error appears in logs, run this sequence first:

  1. Identify the exact dereference line from the stack trace.
  2. Log the variable state immediately before that line.
  3. Check whether the source collection was initialised and populated.
  4. Validate query assumptions (missing parent/child rows, filter drift).
  5. Confirm helper methods return empty collections rather than null.
  6. Reproduce using the same profile/record shape as the failing transaction.

Testing strategy that catches these failures earlier

Write tests for both happy path and sparse-data path.

@isTest
private class InvoiceServiceTest {
    @isTest
    static void returnsEmptyMapWhenInputEmpty() {
        Map<Id, Decimal> totals = InvoiceService.loadInvoiceTotals(new Set<Id>());
        System.assertNotEquals(null, totals);
        System.assertEquals(0, totals.size());
    }

    @isTest
    static void noNullPointerWhenAccountHasNoInvoices() {
        Account a = new Account(Name = 'No Invoice Account');
        insert a;

        Test.startTest();
        Map<Id, Decimal> totals = InvoiceService.loadInvoiceTotals(new Set<Id>{a.Id});
        Test.stopTest();

        System.assertNotEquals(null, totals);
        System.assertEquals(false, totals.containsKey(a.Id));
    }
}

If your tests only assert success with complete fixture data, you will miss the production failure path.

Conclusion

Null Pointer Exceptions in Apex collection workflows are usually contract problems, not language problems.

Use these defaults to reduce failures quickly:

  • Initialise collections early.
  • Return empty collections from collection-returning methods.
  • Guard map/list lookups before dereferencing.
  • Test sparse and partial-data scenarios, not only ideal data.

If you are seeing recurring collection-related failures in triggers, queueables, or batch jobs, fix method contracts first. That gives the fastest reliability gain with the least architectural churn.

Need help stabilising Apex failures in production?

I take short specialist contracts to diagnose recurring Apex failures and harden collection-heavy workflows.

Discuss your Apex issue