Skip to main content

Apex Exception Handling: Complete Guide to Try-Catch, Custom Exceptions, & Best Practices

Updated Add a comment
In plain words: Think of an exception as a surprise roadblock during code execution. Exception handling is your detour plan—using 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.

Real-World Example: Multi-Catch Block Implementation
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.

Step-by-Step: Declaring and Throwing a Custom Business 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());
        }
    }
}
360 Exception Architecture Card:
  • 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

Developer Trap: Empty Catch Blocks & Lost Rollbacks
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.
Core Rule: Never swallow exceptions in empty catch blocks. Always log the error details with 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 call Database.rollback(sp); inside the catch block if any part fails.
  • Leverage the finally Block: Use finally blocks 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.

Was this article helpful?

Have a question or spotted a mistake? Ask here about this article

Found this guide useful? Buy me a coffee (opens in a new tab)

Comments

Questions, corrections and your own experience are all welcome.