Exception Handling in Apex

0/9 in this phase0/41 across the roadmap

📖 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:

  1. Catch specific exceptions, not generic Exception
  2. Always log exception details (message, stack trace, line number)
  3. Use custom exceptions for business logic errors
  4. Never swallow exceptions silently (empty catch blocks)
  5. Consider creating an Error_Log__c object for persistent error tracking
  6. Use addError() on SObject for user-facing validation errors

💻 Code Example

codeTap to expand ⛶
1// Exception Handling — Production Patterns
2
3public class ExceptionHandlingExamples {
4
5 // 1. Basic try-catch patterns
6 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 code
18 System.debug('Query attempt completed');
19 }
20 }
21
22 // 2. DML Exception handling
23 public static void handleDMLErrors(List<Account> accounts) {
24 Database.SaveResult[] results = Database.insert(accounts, false);
25
26 List<Error_Log__c> errorLogs = new List<Error_Log__c>();
27
28 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 }
43
44 if (!errorLogs.isEmpty()) {
45 insert errorLogs; // Log errors to custom object
46 }
47 }
48
49 // 3. Custom Exception classes
50 public class BusinessLogicException extends Exception {}
51 public class IntegrationException extends Exception {}
52 public class ValidationException extends Exception {
53 public String fieldName;
54
55 public ValidationException(String field, String message) {
56 this(message);
57 this.fieldName = field;
58 }
59 }
60
61 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 }
68
69 if (acc.Industry == 'Restricted') {
70 throw new BusinessLogicException(
71 'Cannot create accounts in the Restricted industry'
72 );
73 }
74 }
75
76 // 4. Trigger-safe error handling (addError)
77 public static void triggerValidation(List<Account> accounts) {
78 // addError() shows error to user WITHOUT throwing an exception
79 // It prevents the individual record from being saved
80 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 field
87 }
88 }
89 // Other valid records in the batch WILL still be saved
90 }
91
92 // 5. Error logging framework (enterprise pattern)
93 public class ErrorLogger {
94 private static List<Error_Log__c> pendingLogs = new List<Error_Log__c>();
95
96 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 }
107
108 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 }
117
118 // Call at the end of your transaction
119 public static void flush() {
120 if (!pendingLogs.isEmpty()) {
121 // Use 'without sharing' to ensure logs are always saved
122 Database.insert(pendingLogs, false);
123 pendingLogs.clear();
124 }
125 }
126 }
127
128 // 6. Retryable operations
129 public static HttpResponse callWithRetry(String endpoint, Integer maxRetries) {
130 Integer attempts = 0;
131 Exception lastException;
132
133 while (attempts < maxRetries) {
134 try {
135 HttpRequest req = new HttpRequest();
136 req.setEndpoint(endpoint);
137 req.setMethod('GET');
138 req.setTimeout(120000); // 2 minutes
139
140 HttpResponse res = new Http().send(req);
141
142 if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
143 return res;
144 }
145
146 if (res.getStatusCode() >= 500) {
147 // Server error — retry
148 attempts++;
149 continue;
150 }
151
152 // Client error — don't retry
153 throw new IntegrationException(
154 'API error ' + res.getStatusCode() + ': ' + res.getBody()
155 );
156
157 } catch (CalloutException e) {
158 lastException = e;
159 attempts++;
160 ErrorLogger.log(e, 'callWithRetry attempt ' + attempts);
161 }
162 }
163
164 throw new IntegrationException(
165 'Failed after ' + maxRetries + ' attempts: ' + lastException?.getMessage()
166 );
167 }
168}

🏋️ Practice Exercise

Exception Handling Exercises:

  1. Create a custom Error_Log__c object with fields for message, stack trace, severity, class name, and timestamp
  2. Build an ErrorLogger utility class that captures exceptions and writes to Error_Log__c
  3. Write a trigger that validates records using addError() with both record-level and field-level errors
  4. Implement a retry mechanism for HTTP callouts with exponential backoff
  5. Write a test class that verifies exceptions are thrown for invalid input (use try-catch in tests)
  6. Create a custom exception hierarchy: AppException → BusinessException, IntegrationException, ValidationException
  7. Handle DmlException in a bulk insert and report which specific records failed and why
  8. Write a batch Apex job with comprehensive error handling in start(), execute(), and finish()
  9. Implement a circuit breaker pattern that stops calling an external API after 3 consecutive failures
  10. 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.