Skip to main content

SF-0330 · Concept · Medium

Availability matrix for Trigger.New, Trigger.NewMap, Trigger.Old and Trigger.Old Map?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

Half the NullPointerExceptions in trigger code come from reading a context variable that doesn’t exist in the current event. The fix is to memorise the availability matrix and check the event before dereferencing.

The matrix

EventTrigger.newTrigger.newMapTrigger.oldTrigger.oldMap
before insertYes (no Ids)No (Ids not assigned yet)NoNo
after insertYes (with Ids)YesNoNo
before updateYesYesYesYes
after updateYesYesYesYes
before deleteNoNoYesYes
after deleteNoNoYesYes
after undeleteYesYesNoNo

A few clauses to internalise:

  1. before insert has no newMap — the platform hasn’t assigned record IDs yet, so a map keyed by Id has no key. Use for (X x : Trigger.new) instead.
  2. Deletes only expose old — there are no “new” records during a delete.
  3. Undelete only exposes new — the records are coming back, so there’s no “old” version.
  4. Trigger.new is editable only in before triggers — modifying Trigger.new in after insert or after update throws a runtime error.

How Trigger.old differs from Trigger.new

Both are typed lists, but they represent different snapshots:

  • Trigger.new — the version of the record as it will be saved (in before) or as it just was saved (in after).
  • Trigger.old — the version of the record before the user’s change.

The classic field-change check uses both:

for (Account a : Trigger.new) {
    Account before = Trigger.oldMap.get(a.Id);
    if (before.Industry != a.Industry) {
        // industry changed in this update
    }
}

This pattern only works in update events — oldMap isn’t populated in insert/delete/undelete.

Why Trigger.newMap is null in before insert

The map key is the record’s Salesforce Id. During before insert, the records haven’t been written to the database — they don’t have Ids yet. The platform skips populating newMap rather than handing you a map with null keys. After insert runs after the save, so by after insert, every record has an Id and newMap is fully populated.

Why Trigger.new is read-only in after

The save is already done. Mutating Trigger.new in an after trigger would mean updating already-saved fields, which the platform would have to write a second time — an implicit recursive DML. So it just disallows it:

trigger AccountAfter on Account (after update) {
    for (Account a : Trigger.new) {
        a.Industry = 'Tech'; // throws: System.FinalException: Record is read-only
    }
}

If you need to change a field after a save, requery, copy, and update explicitly — but better still, do the work in a before update trigger to avoid the second DML.

A safe-access helper

A common production pattern is a small handler-class helper that hides the matrix behind named methods:

public abstract class TriggerHandler {
    protected List<SObject> newRecs() {
        return Trigger.new == null ? new List<SObject>() : Trigger.new;
    }
    protected Map<Id, SObject> oldMap() {
        return Trigger.oldMap == null ? new Map<Id, SObject>() : Trigger.oldMap;
    }
    // ...
}

Now downstream code calls newRecs() and never branches on which event fired.

Common interview follow-ups

  • Why is oldMap null in inserts? — Because there’s no prior version of a brand-new record.
  • Can I look up a record’s old value during after delete? — Yes — Trigger.old and Trigger.oldMap are both populated; they’re the only way to see what was deleted.
  • Why does Trigger.new exist in after insert if I can’t change it? — So you can run logic that needs the Ids — typically creating related records, queuing async jobs, sending events.

What interviewers are really looking for

Reciting the matrix is the start. The senior signal is explaining whynewMap is empty in before insert because Ids don’t exist yet, Trigger.new is read-only in after because the save is committed. Mention that mutating Trigger.new in after throws FinalException, and that field-change detection requires the update event to compare oldMap against new.

Verified against: Apex Developer Guide — Trigger Context Variables. Last reviewed 2026-05-17.