Exception Handling in Apex
📖 Concept
Robust exception handling is what separates production-grade code from prototype code. In Salesforce, exceptions can come from DML failures, callout errors, governor limit violations, and business logic errors.
Exception hierarchy:
Exception (base class)
├── DmlException — DML operation failures
├── QueryException — SOQL issues (too many rows, etc.)
├── CalloutException — HTTP callout failures
├── JSONException — JSON parsing errors
├── MathException — Division by zero, etc.
├── NullPointerException — Accessing null reference
├── LimitException — Governor limit exceeded (CANNOT be caught!)
├── TypeException — Invalid type casting
├── ListException — List index out of bounds
├── SObjectException — SObject field access issues
└── CustomException — Your own exception classes
Critical rule: LimitException (governor limit exceeded) CANNOT be caught in try-catch. Once a limit is hit, the entire transaction is rolled back. This is by design — you must prevent limits, not catch them.
Best practices:
- Catch specific exceptions, not generic Exception
- Always log exception details (message, stack trace, line number)
- Use custom exceptions for business logic errors
- Never swallow exceptions silently (empty catch blocks)
- Consider creating an Error_Log__c object for persistent error tracking
- Use addError() on SObject for user-facing validation errors
💻 Code Example
1// Exception Handling — Production Patterns23public class ExceptionHandlingExamples {45 // 1. Basic try-catch patterns6 public static void basicHandling() {7 try {8 Account acc = [SELECT Id, Name FROM Account WHERE Name = 'Nonexistent' LIMIT 1];9 } catch (QueryException e) {10 System.debug('Query failed: ' + e.getMessage());11 } catch (Exception e) {12 System.debug('Unexpected error: ' + e.getMessage());13 System.debug('Stack trace: ' + e.getStackTraceString());14 System.debug('Line number: ' + e.getLineNumber());15 System.debug('Type: ' + e.getTypeName());16 } finally {17 // Always executes — cleanup code18 System.debug('Query attempt completed');19 }20 }2122 // 2. DML Exception handling23 public static void handleDMLErrors(List<Account> accounts) {24 Database.SaveResult[] results = Database.insert(accounts, false);2526 List<Error_Log__c> errorLogs = new List<Error_Log__c>();2728 for (Integer i = 0; i < results.size(); i++) {29 if (!results[i].isSuccess()) {30 for (Database.Error err : results[i].getErrors()) {31 errorLogs.add(new Error_Log__c(32 Object_Name__c = 'Account',33 Record_Name__c = accounts[i].Name,34 Error_Message__c = err.getMessage(),35 Status_Code__c = String.valueOf(err.getStatusCode()),36 Fields__c = String.valueOf(err.getFields()),37 Timestamp__c = Datetime.now(),38 Class_Name__c = 'ExceptionHandlingExamples.handleDMLErrors'39 ));40 }41 }42 }4344 if (!errorLogs.isEmpty()) {45 insert errorLogs; // Log errors to custom object46 }47 }4849 // 3. Custom Exception classes50 public class BusinessLogicException extends Exception {}51 public class IntegrationException extends Exception {}52 public class ValidationException extends Exception {53 public String fieldName;5455 public ValidationException(String field, String message) {56 this(message);57 this.fieldName = field;58 }59 }6061 public static void useCustomExceptions(Account acc) {62 if (acc.AnnualRevenue == null || acc.AnnualRevenue < 0) {63 throw new ValidationException(64 'AnnualRevenue',65 'Annual revenue must be a positive number'66 );67 }6869 if (acc.Industry == 'Restricted') {70 throw new BusinessLogicException(71 'Cannot create accounts in the Restricted industry'72 );73 }74 }7576 // 4. Trigger-safe error handling (addError)77 public static void triggerValidation(List<Account> accounts) {78 // addError() shows error to user WITHOUT throwing an exception79 // It prevents the individual record from being saved80 for (Account acc : accounts) {81 if (String.isBlank(acc.Name)) {82 acc.addError('Account name cannot be blank');83 }84 if (acc.AnnualRevenue != null && acc.AnnualRevenue < 0) {85 acc.AnnualRevenue.addError('Revenue cannot be negative');86 // Field-level error — shows on the specific field87 }88 }89 // Other valid records in the batch WILL still be saved90 }9192 // 5. Error logging framework (enterprise pattern)93 public class ErrorLogger {94 private static List<Error_Log__c> pendingLogs = new List<Error_Log__c>();9596 public static void log(Exception e, String context) {97 pendingLogs.add(new Error_Log__c(98 Error_Message__c = e.getMessage(),99 Stack_Trace__c = e.getStackTraceString()?.left(32000),100 Class_Name__c = context,101 Error_Type__c = e.getTypeName(),102 Line_Number__c = e.getLineNumber(),103 Timestamp__c = Datetime.now(),104 User__c = UserInfo.getUserId()105 ));106 }107108 public static void log(String message, String context, String severity) {109 pendingLogs.add(new Error_Log__c(110 Error_Message__c = message,111 Class_Name__c = context,112 Severity__c = severity,113 Timestamp__c = Datetime.now(),114 User__c = UserInfo.getUserId()115 ));116 }117118 // Call at the end of your transaction119 public static void flush() {120 if (!pendingLogs.isEmpty()) {121 // Use 'without sharing' to ensure logs are always saved122 Database.insert(pendingLogs, false);123 pendingLogs.clear();124 }125 }126 }127128 // 6. Retryable operations129 public static HttpResponse callWithRetry(String endpoint, Integer maxRetries) {130 Integer attempts = 0;131 Exception lastException;132133 while (attempts < maxRetries) {134 try {135 HttpRequest req = new HttpRequest();136 req.setEndpoint(endpoint);137 req.setMethod('GET');138 req.setTimeout(120000); // 2 minutes139140 HttpResponse res = new Http().send(req);141142 if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {143 return res;144 }145146 if (res.getStatusCode() >= 500) {147 // Server error — retry148 attempts++;149 continue;150 }151152 // Client error — don't retry153 throw new IntegrationException(154 'API error ' + res.getStatusCode() + ': ' + res.getBody()155 );156157 } catch (CalloutException e) {158 lastException = e;159 attempts++;160 ErrorLogger.log(e, 'callWithRetry attempt ' + attempts);161 }162 }163164 throw new IntegrationException(165 'Failed after ' + maxRetries + ' attempts: ' + lastException?.getMessage()166 );167 }168}
🏋️ Practice Exercise
Exception Handling Exercises:
- Create a custom Error_Log__c object with fields for message, stack trace, severity, class name, and timestamp
- Build an ErrorLogger utility class that captures exceptions and writes to Error_Log__c
- Write a trigger that validates records using addError() with both record-level and field-level errors
- Implement a retry mechanism for HTTP callouts with exponential backoff
- Write a test class that verifies exceptions are thrown for invalid input (use try-catch in tests)
- Create a custom exception hierarchy: AppException → BusinessException, IntegrationException, ValidationException
- Handle DmlException in a bulk insert and report which specific records failed and why
- Write a batch Apex job with comprehensive error handling in start(), execute(), and finish()
- Implement a circuit breaker pattern that stops calling an external API after 3 consecutive failures
- Create a dashboard that shows error trends from your Error_Log__c object
⚠️ Common Mistakes
Catching LimitException — it CANNOT be caught. Once a governor limit is exceeded, the entire transaction rolls back. Prevent limits, don't try to catch them
Empty catch blocks (swallowing exceptions) — 'catch (Exception e) {}' hides errors and makes debugging impossible. Always log or rethrow
Using generic Exception catch for everything — catch specific exceptions first (DmlException, CalloutException) so you can handle each appropriately
Not using addError() in triggers — throwing exceptions in after-triggers causes ALL records to fail. Use addError() to fail individual records
Not logging the stack trace — e.getMessage() alone is often insufficient for debugging. Always log e.getStackTraceString() and e.getLineNumber()
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Exception Handling in Apex. Login to unlock this feature.