Intermediate 30 min read

Add Validation in Apex

By the end of this lesson, you'll be able to:

  • Prevent an Opportunity from being marked Closed Won without a positive Amount
  • Use addError() to block a save with a field-level message
  • Explain why this validation belongs in a before trigger, not after

Prerequisites: "Bulkify the Logic"

The validation rule

public void beforeUpdate(List<Opportunity> newOpportunities, Map<Id, Opportunity> oldMap) {
    for (Opportunity opp : newOpportunities) {
        Opportunity oldOpp = oldMap.get(opp.Id);
        Boolean justWon = opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won';

        if (justWon && (opp.Amount == null || opp.Amount <= 0)) {
            opp.Amount.addError('An Opportunity cannot be marked Closed Won without a positive Amount.');
        }
    }
}
  1. public void beforeUpdate(List<Opportunity> newOpportunities, Map<Id, Opportunity> oldMap) { — a new method, separate from afterUpdate.
  2. for (Opportunity opp : newOpportunities) { — loops over the updated records.
  3. Opportunity oldOpp = oldMap.get(opp.Id); — the prior version.
  4. Boolean justWon = opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won'; — the same transition check as before.
  5. (blank line)
  6. if (justWon && (opp.Amount == null || opp.Amount <= 0)) { — only validates on the actual transition, and only when the amount is missing or non-positive.
  7. opp.Amount.addError('An Opportunity cannot be marked Closed Won without a positive Amount.'); — addError(), called on the specific field, blocks the entire save and shows the message directly next to that field.
  8. } — closes the if.
  9. } — closes the loop.
  10. } — closes the method.

Why this belongs in before, not after

addError() only works meaningfully in a before trigger — calling it in an after trigger doesn't prevent the save, since the record is already saved by the time after runs (Module 24's before/after distinction, once again). This validation needs its own beforeUpdate method on the handler, separate from the rollup logic living in afterUpdate.

Updating the trigger to route both events

trigger OpportunityTrigger on Opportunity (before update, after update) {
    OpportunityTriggerHandler handler = new OpportunityTriggerHandler();

    if (Trigger.isBefore) {
        handler.beforeUpdate(Trigger.new, Trigger.oldMap);
    }
    if (Trigger.isAfter) {
        handler.afterUpdate(Trigger.new, Trigger.oldMap);
    }
}
  1. trigger OpportunityTrigger on Opportunity (before update, after update) { — this is Module 24's "Trigger Framework Fundamentals" routing pattern, now genuinely needed — one trigger, two events.
  2. OpportunityTriggerHandler handler = new OpportunityTriggerHandler(); — one handler instance.
  3. (blank line)
  4. if (Trigger.isBefore) { — routes the before-timing event.
  5. handler.beforeUpdate(Trigger.new, Trigger.oldMap); — to its own clearly-named method.
  6. } — closes the if.
  7. if (Trigger.isAfter) { — routes the after-timing event.
  8. handler.afterUpdate(Trigger.new, Trigger.oldMap); — to its own method.
  9. } — closes the if.
  10. } — closes the trigger.

Exercise

Add a validation that blocks any Opportunity update where StageName is being set to 'Closed Won' but CloseDate is null, using addError() on CloseDate.

Show hint

opp.CloseDate.addError(...)

Salesforce: ○ Not connected Connect
APEX

Execution Results

Run your code to see the result.

Add Validation in Apex Quiz

1. Why must addError() be called in a before trigger to actually block the save?

Log in to submit the quiz and save your score.

My Notes

Log in to keep private notes on this lesson.

Questions about this lesson

No questions yet — be the first to ask.

Log in to ask a question about this lesson.

Summary

A Closed Won Opportunity with no Amount would roll up as 0, silently hiding real revenue — this lesson blocks that scenario at the source with a before-trigger validation.