The pattern: catch specific exception types first, then fall back to a generic Exception. The compiler routes a thrown exception to the first matching catch block, so ordering matters.
A full worked example
public with sharing class AccountUpdater {
public static void renameAccount(Id accountId, String newName) {
try {
// Specific failure modes we want to handle differently
Account a = [SELECT Id, Name FROM Account WHERE Id = :accountId LIMIT 1];
a.Name = newName;
update a;
} catch (QueryException qe) {
// Specific: no row, too many rows, malformed SOQL
System.debug('Lookup failed: ' + qe.getMessage());
throw new AccountUpdaterException('Account not found: ' + accountId, qe);
} catch (DmlException de) {
// Specific: validation rule, required field, duplicate, sharing
System.debug('Save failed: ' + de.getDmlMessage(0));
throw new AccountUpdaterException('Could not save account', de);
} catch (Exception e) {
// Generic: catches anything else (null pointer, type cast, etc.)
System.debug('Unexpected error: ' + e.getTypeName() + ' — ' + e.getMessage());
throw new AccountUpdaterException('Unexpected error renaming account', e);
}
}
public class AccountUpdaterException extends Exception {}
}
Three things to notice:
- Specific first.
QueryExceptionandDmlExceptionare checked before the wildcardExceptioncatch. IfExceptioncame first the compiler would refuse to build — unreachable code. - Different reactions per type. A query miss probably means a bad input, so we surface a user-friendly “Not found” message. A DML failure is a save problem, so we surface the field-level error. The generic catch is the safety net for everything we didn’t anticipate.
- Wrapping preserves the cause. Re-throwing as a domain-specific
AccountUpdaterExceptionkeeps callers from depending on Salesforce’s internal types, but passingqe/de/eas the second argument preserves the original stack trace viagetCause().
Single specific catch
If you only care about one failure mode:
try {
insert new Contact(LastName = 'Smith');
} catch (DmlException de) {
for (Integer i = 0; i < de.getNumDml(); i++) {
System.debug(de.getDmlFieldNames(i) + ' — ' + de.getDmlMessage(i));
}
}
getDmlFieldNames(i) gives you the field-level cause for row i, which is gold for showing a useful message to the user.
Single generic catch
For “log everything, fail nothing critical”:
try {
sendOptionalEmail();
} catch (Exception e) {
Logger.logException(e); // capture and move on
}
This pattern is for fire-and-forget side effects (analytics pings, cache warmers) — never for primary business logic. Swallowing a critical exception silently is one of the worst bugs you can ship in a multi-tenant environment.
Common follow-ups
- Can you re-order generic-first? — No. The compiler rejects unreachable catch blocks.
- How many catches per try? — As many as you have exception types, but only one of each type.
- Should every method catch? — No. Let exceptions bubble to the boundary (controller, REST endpoint, trigger entry point) and catch there. Catch-too-early hides real bugs.
Verified against: Apex Developer Guide — Catching Different Exception Types. Last reviewed 2026-05-17 for Spring ‘26.