02 - Apex: Triggers, Classes, Exceptions¶
Apex class basics¶
public class AccountService {
// Static method (class-level)
public static void updateAllAccountsRevenue() { }
// Instance method
public void updateRevenue(List<Account> accs) { }
// Constructor
public AccountService() { }
}
Access modifiers¶
public- within namespaceprivate- within class onlyglobal- cross-namespace (managed packages)protected- inheriting classesvirtual- allows overridingabstract- must overridefinal- cannot override
Sharing modes¶
public with sharing class X- enforces user's record sharingpublic without sharing class X- bypasses sharing (system context)public inherited sharing class X- inherits from caller; safer default for utility classes
Triggers default to without sharing unless declared on a with sharing class. Most utility services use with sharing to respect user permissions.
Apex Triggers¶
trigger AccountTrigger on Account (before insert, before update,
after insert, after update,
before delete, after delete, after undelete) {
// Logic goes here
}
Trigger context¶
Trigger.new- List of new sObject versions (before/after insert/update; null on delete)Trigger.old- List of old sObject versions (before/after update/delete; null on insert/undelete)Trigger.newMap- Mapof new records (after insert/update only since records have IDs) Trigger.oldMap- Mapof old records (update/delete) - Boolean flags:
Trigger.isBefore,isAfter,isInsert,isUpdate,isDelete,isUndelete,isExecuting Trigger.size- record countTrigger.operationType- enum (BEFORE_INSERT, AFTER_UPDATE, etc.)
Operation timing¶
| Operation | Use |
|---|---|
| before insert | Modify field on the record being inserted; validate before save |
| before update | Modify field on update before save; validate |
| before delete | Prevent deletion via addError() |
| after insert | Need the record's Id; create related records; integrate downstream |
| after update | Update related records; trigger workflows |
| after delete | Cleanup related data |
| after undelete | React to record restored from Recycle Bin |
Best practices (memorize)¶
- One trigger per object - use a dispatcher class with handlers per event
- Bulkify - work with
Trigger.newas a List, never assume single record - No SOQL/DML inside loops - aggregate IDs first, query outside, update in bulk
- Logic in handler classes - keep triggers thin
- Recursion control - static class flag to prevent re-entry
- Error handling -
addError()for validation; throw exception for fatal errors
Bulkified pattern example¶
trigger ContactTrigger on Contact (after insert, after update) {
Set<Id> accountIds = new Set<Id>();
for (Contact c : Trigger.new) {
if (c.AccountId != null) accountIds.add(c.AccountId);
}
// Single SOQL for all related records
Map<Id, Account> accs = new Map<Id, Account>(
[SELECT Id, Total_Contacts__c FROM Account WHERE Id IN :accountIds]
);
// Logic
for (Contact c : Trigger.new) {
if (c.AccountId != null) {
Account a = accs.get(c.AccountId);
a.Total_Contacts__c = (a.Total_Contacts__c == null ? 0 : a.Total_Contacts__c) + 1;
}
}
// Single DML
update accs.values();
}
Trigger handler pattern¶
trigger AccountTrigger on Account (before insert, after update) {
new AccountTriggerHandler().run();
}
public class AccountTriggerHandler {
public void run() {
if (Trigger.isBefore && Trigger.isInsert) beforeInsert(Trigger.new);
if (Trigger.isAfter && Trigger.isUpdate) afterUpdate(Trigger.new, Trigger.oldMap);
}
private void beforeInsert(List<Account> newAccounts) {
for (Account a : newAccounts) {
if (a.Name == null) a.Name = 'Default';
}
}
private void afterUpdate(List<Account> newAccs, Map<Id, Account> oldMap) {
// ...
}
}
Recursion guard¶
Trigger recursion happens when DML inside a trigger fires the same trigger again.
public class TriggerHelper {
public static Boolean accountTriggerRunning = false;
}
trigger AccountTrigger on Account (after update) {
if (TriggerHelper.accountTriggerRunning) return;
TriggerHelper.accountTriggerRunning = true;
try {
// Logic
} finally {
TriggerHelper.accountTriggerRunning = false;
}
}
Exception handling¶
try {
insert new Account(Name=null); // will throw
} catch (DmlException e) {
System.debug(e.getMessage());
} catch (Exception e) {
// catch-all
} finally {
// cleanup
}
Custom exceptions¶
public class MyAppException extends Exception {}
throw new MyAppException('Custom error');
Custom exceptions must extend Exception.
Common Apex exceptions¶
DmlException- DML failedQueryException- SOQL failed (e.g., zero rows when single expected)LimitException- hit a governor limit (cannot be caught and resumed)NullPointerExceptionTypeException- type cast failureSObjectException- sObject misuse
addError() for graceful failure in triggers¶
trigger AccountTrigger on Account (before delete) {
for (Account a : Trigger.old) {
if (a.Type == 'Critical') {
a.addError('Cannot delete critical accounts.');
}
}
}
This blocks the operation and returns a user-friendly error - cleaner than throwing exceptions.
Common exam triggers¶
- "Bulkify a trigger" β Use Maps/Sets, no SOQL/DML in loops
- "Modify field on the record being inserted" β before insert / before update
- "Need the new record's Id" β after insert (Id is populated)
- "Block deletion of critical records" β before delete with
addError() - "Trigger fires twice for one record" β recursion; add static flag guard
- "Multiple triggers on Account doing different things" β consolidate into one trigger + dispatcher pattern
- "Catch DML failure and continue" β
Database.insert(records, false)returns SaveResult[] without throwing on partial failure - "Custom error message" β
addError()(in before-trigger context) or throw custom exception