Try, Catch, Finally, and Exception Types
Apex exception handling follows the familiar try/catch/finally structure: code in the try block executes normally, a matching catch block intercepts a thrown exception of a specific type (or its supertype), and the optional finally block always executes whether or not an exception occurred, making it the right place for cleanup logic. Apex provides built-in exception types like DmlException, QueryException, NullPointerException, ListException, and CalloutException, each thrown automatically by the runtime for the corresponding failure category, and every custom exception class you define by extending Exception (e.g., class InsufficientInventoryException extends Exception {}) automatically inherits getMessage(), getStackTraceString(), getTypeName(), and getCause().
Cricket analogy: It's like a batsman having a pre-planned response for specific dismissal types — a review for a marginal LBW versus simply walking off for a clean bowled — rather than reacting the same way to every wicket.
DML Exception Handling and Partial Success
For bulk DML operations, wrapping insert/update statements in try/catch stops processing entirely on the first failed record within that statement by default; to allow some records to succeed while others fail, use Database.insert(records, false) or Database.update(records, false), which returns a list of Database.SaveResult objects that you inspect individually with .isSuccess() and .getErrors() instead of throwing. This partial-success pattern is essential in batch and bulk contexts where a single bad record — say, a duplicate external ID — shouldn't cause every other valid record in the same chunk to roll back.
Cricket analogy: It's like a bowling attack continuing the innings even after one bowler has a disastrous over — the team doesn't forfeit the whole match, they just note that over's damage and carry on with the rest of the spell.
List<Account> accountsToInsert = buildAccountsFromImport();
Database.SaveResult[] results = Database.insert(accountsToInsert, false);
List<String> failureLog = new List<String>();
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
for (Database.Error err : results[i].getErrors()) {
failureLog.add('Row ' + i + ': ' + err.getStatusCode() + ' - ' + err.getMessage());
}
}
}
if (!failureLog.isEmpty()) {
System.debug(LoggingLevel.ERROR, String.join(failureLog, '\n'));
}Custom Exceptions and Rethrowing
Defining domain-specific custom exceptions — like InsufficientInventoryException or InvalidDiscountException — makes calling code's catch blocks far more expressive than catching a generic Exception, and you can chain the original cause into a custom exception via the two-argument constructor pattern (new MyException('context message', originalException)) so getCause() preserves the full root failure for logging. In trigger and service-layer code, it's a best practice to catch narrow, expected exceptions close to where they occur and only let truly unexpected exceptions propagate up, since an uncaught exception in a trigger rolls back the entire DML operation that fired it, including any partial work already done in that transaction.
Cricket analogy: It's like a scorer recording not just 'out' but the precise dismissal type — caught behind, run out, stumped — preserving the specific cause rather than a vague generic entry in the scorebook.
Catching System.Exception broadly and swallowing it silently (an empty catch block) is a serious anti-pattern — it hides bugs like NullPointerExceptions in production and makes support tickets nearly impossible to diagnose; always log or rethrow.
Use Test.startTest()/Test.stopTest() combined with a try/catch and System.assert on the exception's getMessage() to unit-test that your code throws the right custom exception under the right failure condition.
- try/catch/finally lets you intercept specific exception types and always run cleanup logic in finally.
- Apex provides built-in exceptions like DmlException, QueryException, NullPointerException, and CalloutException.
- Custom exceptions extend Exception and inherit getMessage(), getStackTraceString(), getTypeName(), and getCause().
- Database.insert/update(records, false) enables partial success, returning SaveResult objects instead of throwing.
- SaveResult.isSuccess() and getErrors() let you inspect per-record DML outcomes in bulk operations.
- Chain an original exception as the cause of a custom exception to preserve root-cause information.
- Never swallow exceptions silently with an empty catch block — always log or rethrow.
Practice what you learned
1. Which block in Apex exception handling always executes, whether or not an exception was thrown?
2. What must a custom Apex exception class do to be valid?
3. How do you allow partial success on a bulk insert instead of failing the whole operation on one bad record?
4. What method on Database.SaveResult tells you whether a specific record succeeded?
5. What happens when an exception goes uncaught inside a trigger?
Was this page helpful?
You May Also Like
Batch Apex
Learn how to process millions of records safely using the Database.Batchable interface, controlling chunk size, state, and job chaining.
Apex REST and Web Services
Learn how to expose custom REST endpoints with @RestResource, handle request/response payloads, and make outbound callouts to external web services.
Future Methods and Async Apex
Learn how @future methods let Apex defer work to a separate asynchronous transaction with its own governor limits, and when to reach for them over other async tools.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics