Handler class indexes Trigger.new / Trigger.old directly

ID

apex.correctness.direct_trigger_map

Severity

high

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Api Design

Language

Apex

Description

Reports indexed access to the trigger context from a class - Trigger.new[i], Trigger.old[i], Trigger.newMap.get(…​) or Trigger.oldMap.get(…​).

Rationale

Half of those collections are null in half of the trigger contexts: Trigger.old and Trigger.oldMap are null on insert, Trigger.new and Trigger.newMap are null on delete, so an unguarded index throws a null dereference the first time the class runs in the other context. Reading the context inside the class also welds it to the trigger: the same logic cannot be called from a batch, from a Queueable or from a test that builds its own records, so the only way to exercise it is to fire a real DML statement and hope the right context happens.

The following code illustrates the pattern detected by this rule:

public class AccountTriggerHandler {

    public void onAfterUpdate() {
        // FLAGGED: Handler class indexes Trigger.new / Trigger.old directly
        Account before = Trigger.old[0];
        // FLAGGED: Handler class indexes Trigger.new / Trigger.old directly
        Account after = Trigger.new[0];
        System.debug(before.Name + after.Name);
    }

    public void compareOwners() {
        for (Account a : (List<Account>) Trigger.new) {
            // FLAGGED: Handler class indexes Trigger.new / Trigger.old directly
            Account previous = Trigger.oldMap.get(a.Id);
            if (previous.OwnerId != a.OwnerId) {
                System.debug('owner changed');
            }
        }
    }

    public void handleAfterUpdate(List<Account> updated, Map<Id, Account> previousById) {
        for (Account a : updated) {
            Account previous = previousById.get(a.Id);
            if (previous.OwnerId != a.OwnerId) {
                System.debug('owner changed');
            }
        }
    }

    public void handleAfterInsert(List<Account> inserted) {
        for (Account a : inserted) {
            System.debug(a.Name);
        }
    }
}

Remediation

Take the records as parameters - handleAfterUpdate(List<Account> updated, Map<Id, Account> previousById) - and let the trigger itself pass Trigger.new and Trigger.oldMap in, once, from the context where they are known to exist. The handler is then callable from a batch and testable without DML.

Configuration

This detector does not need any configuration.

© 2026 Xygeni. All rights reserved