try-catch blocks to catch unexpected errors, log what went wrong, and keep your application running without crashing the user interface.
In Salesforce development, writing defensive Apex code means planning for real-world failures: failed validation rules, missing records, locked database rows, and network timeouts. Implementing structured exception handling ensures that unexpected errors fail gracefully, preserve data integrity, and provide actionable feedback to users and developers.
1. Understanding Built-In Apex Exception Types
Salesforce provides specific built-in exception classes derived from the base System.Exception class. Catching specific exceptions rather than generic ones helps you write targeted recovery logic:
DmlException: Thrown when a database record insert, update, or delete violates validation rules, required fields, or foreign key constraints.QueryException: Thrown when a single-record SOQL query returns zero rows or more than one row (e.g.,Account acc = [SELECT Id FROM Account WHERE Name = 'Acme'];).ListException: Occurs when attempting to access an invalid list index (such as requesting index 5 on an empty list).NullPointerException: Triggers when dereferencing or invoking a method on a variable that has not been initialized.AuraHandledException: A specialized exception used to pass user-friendly, localized error messages from Apex controllers to Lightning Web Components (LWC).
2. Catching Specific Exceptions in Order
When handling exceptions, always place more specific exception catch blocks above the generic Exception handler. Apex processes catch blocks top to bottom and executes the first matching block.
Handling database and query errors separately before catching unhandled system exceptions:
public with sharing class OpportunityInvoiceService {
public static void processInvoice(Id oppId) {
try {
Opportunity opp = [
SELECT Id, Amount, StageName
FROM Opportunity
WHERE Id = :oppId
WITH USER_MODE
LIMIT 1
];
Invoice__c inv = new Invoice__c(
Opportunity__c = opp.Id,
Total_Amount__c = opp.Amount,
Status__c = 'Draft'
);
insert as user inv;
} catch (QueryException qe) {
System.debug(LoggingLevel.ERROR, 'Opportunity record not found: ' + qe.getMessage());
throw new AuraHandledException('Unable to locate the specified Opportunity.');
} catch (DmlException de) {
System.debug(LoggingLevel.ERROR, 'DML failure creating invoice: ' + de.getDmlMessage(0));
throw new AuraHandledException('Database error: ' + de.getDmlMessage(0));
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'Unexpected error: ' + e.getMessage() + ' | Stack: ' + e.getStackTraceString());
throw new AuraHandledException('An unexpected system error occurred. Please contact support.');
}
}
}
3. Defining and Throwing Custom Exceptions
To enforce custom business logic and domain rules, define custom exception classes by extending Exception. Custom exception class names must always end with the word Exception.
// 1. Declare the custom exception class
public class OrderValidationException extends Exception {}
// 2. Throw and catch the custom exception in business logic
public class OrderService {
public static void validateAndSubmit(Order ord) {
try {
if (ord.TotalAmount <= 0) {
throw new OrderValidationException('Orders must have a total value greater than $0.00.');
}
ord.Status = 'Activated';
update as user ord;
} catch (OrderValidationException ove) {
System.debug(LoggingLevel.WARN, 'Validation blocked order submission: ' + ove.getMessage());
throw new AuraHandledException(ove.getMessage());
}
}
}
getMessage(): Returns the error message string associated with the caught exception.getStackTraceString(): Returns the complete stack trace and line number where the exception occurred.getDmlMessage(index): Retrieves the exact field-level validation message for failed DML rows.- Uncatchable Limits: Governor limit exceptions (e.g.,
LimitException: Too many SOQL queries: 101) and CPU timeouts cannot be caught with try-catch blocks.
4. Common Traps & Exception Handling Best Practices
Writing
catch (Exception e) {} swallows exceptions silently, hiding critical bugs and allowing broken processes to continue. Furthermore, catching an exception around a DML operation without re-throwing or setting a Savepoint prevents the automatic database rollback, leaving your database in an inconsistent state.
getStackTraceString(), roll back uncommitted work with Database.rollback() if needed, and surface clean errors via AuraHandledException.
- Use Transaction Savepoints for Multi-DML Workflows: Set a savepoint (
Savepoint sp = Database.setSavepoint();) before executing complex operations, and callDatabase.rollback(sp);inside the catch block if any part fails. - Leverage the
finallyBlock: Usefinallyblocks to execute cleanup actions (such as releasing resources or logging metrics) that must run whether an exception was thrown or not. - Log to Persistent Storage: In-memory debug logs expire and can be truncated. Publish errors to a custom logging object or fire an event-driven Platform Event in catch blocks for long-term monitoring.
Summary
Robust exception handling is essential for maintaining scalable and dependable Salesforce applications. By catching specific system exceptions, creating custom exception types for business rules, handling transaction rollbacks cleanly, and surfacing user-friendly messages with AuraHandledException, you protect your data integrity and deliver a reliable CRM experience.
Comments
Questions, corrections and your own experience are all welcome.
Post a Comment