Exception statements are the four Apex keywords that drive error handling: try, catch, finally, and throw. Together they let you isolate risky work, react to specific failures, run cleanup unconditionally, and raise your own errors.
The four keywords
| Keyword | Job |
|---|---|
try | Wraps a block of code that might throw |
catch | Handles a specific exception type. You can have multiple, ordered specific-to-general |
finally | Always runs after try/catch — success, exception, or even return |
throw | Raises an exception (a built-in type or your own custom class) |
Full shape
try {
// risky work
Account a = [SELECT Id FROM Account WHERE Name = 'NoSuch' LIMIT 1];
insert new Contact(LastName = 'X', AccountId = a.Id);
} catch (QueryException qe) {
System.debug('Account lookup failed: ' + qe.getMessage());
} catch (DmlException de) {
System.debug('Contact insert failed: ' + de.getMessage());
} catch (Exception e) {
System.debug('Other failure: ' + e.getMessage());
} finally {
System.debug('Always runs — release locks, close streams, etc.');
}
Order matters: specific exception types must appear before generic ones. catch (Exception e) is the wildcard — if you put it first, more specific catches below it are dead code, and the compiler flags it.
Throwing your own
public class ValidationException extends Exception {}
public class AccountService {
public static void validate(Account a) {
if (a.Name == null) {
throw new ValidationException('Account Name is required');
}
}
}
Custom exception classes simply extend Exception — no method overrides required. They give you a typed catch block in the caller, which is much cleaner than parsing message strings.
Rethrowing
Sometimes you catch an exception just to log it, then re-throw so the caller still sees the failure:
try {
riskyWork();
} catch (Exception e) {
Logger.log(e);
throw e;
}
You can also wrap a low-level exception in a higher-level one and preserve the cause:
} catch (DmlException de) {
throw new AccountServiceException('Failed to save account', de);
}
What finally is for
The classic uses on the platform:
- Resetting
Test.setCurrentPage(...)or any other test-only state - Releasing a manually-acquired lock (rare in Apex)
- Logging exit conditions so debug logs are self-contained
- Re-enabling a triggers-disabled flag
finally runs even if a return statement fires inside try. The one thing that won’t run a finally is a LimitException (governor limit) — the runtime is killing the transaction outright.
Common follow-ups
- Can you have
trywithoutcatch? — Only if you havefinally.try {} finally {}is legal;try {}alone is not. - Multiple catches per type? — No, one catch per exception type per
try. - Catching
LimitException? — Not catchable in normal code paths.
Verified against: Apex Developer Guide — Exception Statements. Last reviewed 2026-05-17 for Spring ‘26.