Skip to content

BeforeUpdate.Validator

Rejects an update with an error message before it is saved. Use it to block a transition or to keep a value from being cleared.

Interface

apex
public class BeforeUpdate {
    public interface Validator extends Handler {
        Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record);
        void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record);
    }
}
  • errorShouldBeAttachedOnBeforeUpdateWhen(record): called once per record in the chunk. Return true to reject the record.
  • addErrorOnBeforeUpdate(record): called right after its predicate returns true; it must attach an error to the record, or the library throws.

Example

apex
public with sharing class ContactValidator implements BeforeUpdate.Validator {
    public Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChangedTo(Contact.Email, null);
    }

    public void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record) {
        record.addError(Contact.Email, 'Email cannot be removed.');
    }
}
cls
public with sharing class OpportunityWinAmountValidator implements BeforeUpdate.Validator {
    public Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChangedTo(Opportunity.StageName, 'Closed Won') && (record.isNull(Opportunity.Amount) || record.lessThanOrEqualTo(Opportunity.Amount, 0));
    }

    public void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record) {
        record.addError(Opportunity.Amount, 'Set a positive Amount before moving this Opportunity to Closed Won.');
    }
}
cls
public with sharing class AccountDemotionValidator implements BeforeUpdate.Validator {
    public Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
        Account oldAccount = (Account) record.getOldSObject();

        return record.isChangedTo(Account.Type, 'Prospect') && oldAccount.Type?.startsWith('Customer') == true;
    }

    public void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record) {
        Account oldAccount = (Account) record.getOldSObject();

        record.addError(Account.Type, 'This account is already ' + oldAccount.Type + ' and cannot be moved back to Prospect. Close it as Lost instead.');
    }
}

Good to Know

  • Gate on a change. A Validator runs on every update, nested ones included, with no recursion limit. Without isChanged, a record that fails the check cannot be updated at all, not even by automation.
  • Name and address fields lose the field. On them, record.addError(field, message) shows the message at record level. To keep it on the field, call ((Account) record.getNewSObject()).BillingCountry.addError(message).
  • Never call addError on getOldSObject(). It throws a FinalException that nothing in the trigger can catch.
  • No DML. If the handler runs DML or publishes an event, the library throws.
  • Populator wins. A class that also implements BeforeUpdate.Populator runs only as a Populator. Its Validator methods never run.

Test

apex
@IsTest
static void errorShouldBeAttachedOnBeforeUpdateWhenWonWithZeroAmount() {
    // Setup
    TriggerHandler.UpdateRecord record = new TriggerHandler.TriggerRecord(
        new Opportunity(StageName = 'Closed Won', Amount = 0),
        new Opportunity(StageName = 'Negotiation/Review')
    );

    // Test
    Boolean result = new OpportunityWinAmountValidator().errorShouldBeAttachedOnBeforeUpdateWhen(record);

    // Verify
    Assert.isTrue(result, 'The record should be rejected.');
}