If you have been working with Salesforce Apex, sooner or later you will see errors like
System.NullPointerException, DmlException or
QueryException.
When this happens, the first question is usually: "Why is Salesforce throwing this error?"
But in a real project, the more important question is: "How should my Apex code handle this error?"
That's where Exception Handling becomes important.
Exception handling is not just about putting every piece of code inside
try-catch. Good exception handling should help us understand the
problem, protect the application, provide a useful message to the user and make
production issues easier to troubleshoot.
Table of Contents
- What is an Exception?
- Why Do We Need Exception Handling?
- Basic Try-Catch
- Exception Methods
- Common Apex Exceptions
- DmlException
- QueryException
- NullPointerException
- ListException
- CalloutException
- JSON Exception Handling
- Custom Exceptions
- Exception Handling with LWC
- Exception Logging
- Database Methods and Partial Success
- Exception Handling in Queueable
- Common Mistakes
- Best Practices
- Salesforce Interview Questions
What is an Exception in Salesforce?
An exception is an error that occurs while Apex code is running.
For example:
Account acc = [
SELECT Id, Name
FROM Account
WHERE Name = 'ABC Company'
];
If there is no matching Account, Apex can throw a
QueryException.
System.QueryException:
List has no rows for assignment to SObject
Another common example is accessing a variable that contains
null.
Account acc;
System.debug(acc.Name);
This can result in:
System.NullPointerException
These runtime problems are called exceptions.
Why Do We Need Exception Handling?
Imagine that you have an LWC where a user clicks a button called Create Application.
The Apex method performs the following operations:
- Get customer details
- Validate the customer
- Create an application
- Call an external API
- Update Salesforce records
Now imagine the external API is down.
Without proper handling, the user might see a generic error such as:
Internal Server Error
A better application can show:
At the same time, the developer can log the actual technical exception for troubleshooting.
Basic Try-Catch in Apex
The most common exception handling structure in Apex is
try-catch.
try {
// Code that may throw an exception
} catch (Exception e) {
// Handle exception
}
For example:
public static void createAccount() {
try {
Account acc = new Account(
Name = 'Test Account'
);
insert acc;
} catch (Exception e) {
System.debug('Error: ' + e.getMessage());
}
}
If the code inside try throws an exception, Salesforce moves
execution to the catch block.
Useful Methods of the Exception Object
The exception object contains useful information that helps developers understand what went wrong.
catch (Exception e) {
System.debug('Message: ' + e.getMessage());
System.debug('Type: ' + e.getTypeName());
System.debug('Line: ' + e.getLineNumber());
System.debug('Stack Trace: ' + e.getStackTraceString());
}
getMessage()
Returns the exception message.
e.getMessage()
getTypeName()
Returns the type of exception.
e.getTypeName()
For example:
System.DmlException
getLineNumber()
Returns the line number where the exception occurred.
e.getLineNumber()
getStackTraceString()
Returns the stack trace, which can be very useful while debugging a production issue.
e.getStackTraceString()
Common Apex Exceptions
| Exception | Common Cause |
|---|---|
DmlException |
Insert, update or delete failure |
QueryException |
SOQL runtime problem |
NullPointerException |
Accessing a null reference |
ListException |
Invalid list operation |
TypeException |
Invalid type conversion |
JSONException |
Invalid JSON processing |
CalloutException |
HTTP or callout problem |
SObjectException |
Invalid SObject field access |
LimitException |
Governor limit exceeded |
1. DmlException
DmlException is one of the most common exceptions in Salesforce.
For example:
Account acc = new Account();
insert acc;
The Account Name field is required, so the DML operation can fail.
We can handle it like this:
try {
Account acc = new Account();
insert acc;
} catch (DmlException e) {
System.debug(
'DML Error: ' + e.getMessage()
);
}
Handling Multiple DML Errors
When processing multiple records, it is sometimes useful to inspect individual DML errors.
List<Account> accounts = new List<Account>();
accounts.add(new Account(Name = 'Account 1'));
accounts.add(new Account());
accounts.add(new Account(Name = 'Account 3'));
try {
insert accounts;
} catch (DmlException e) {
for (Integer i = 0; i < e.getNumDml(); i++) {
System.debug(
'Record ' + i +
' failed: ' +
e.getDmlMessage(i)
);
}
}
2. QueryException
Consider this query:
Account acc = [
SELECT Id, Name
FROM Account
WHERE Name = 'ABC'
];
If there is no matching record, assigning the query directly to an sObject can
result in a QueryException.
A common approach is to query into a list when zero records is an expected possibility.
List<Account> accounts = [
SELECT Id, Name
FROM Account
WHERE Name = 'ABC'
];
if (!accounts.isEmpty()) {
Account acc = accounts[0];
}
try-catch. If "no record found" is a normal business situation,
handle it using normal conditional logic.
3. NullPointerException
This is probably one of the errors Salesforce developers encounter very often.
Account acc;
System.debug(acc.Name);
Here acc is null.
Before accessing the variable, check it:
if (acc != null) {
System.debug(acc.Name);
}
The same idea applies to relationship fields.
if (con != null &&
con.Account != null &&
con.Account.Name == 'ABC') {
// Business logic
}
4. ListException
Consider the following code:
List<Account> accounts = new List<Account>();
Account acc = accounts[0];
The list is empty, so trying to access index 0 can cause:
System.ListException:
List index out of bounds
A simple check prevents this:
if (!accounts.isEmpty()) {
Account acc = accounts[0];
}
5. CalloutException
Salesforce applications frequently communicate with external systems.
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint(
'https://api.example.com/customer'
);
request.setMethod('GET');
HttpResponse response = http.send(request);
The external service may be unavailable, timeout, or reject the request.
A callout exception can be handled like this:
try {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint(
'https://api.example.com/customer'
);
request.setMethod('GET');
HttpResponse response =
http.send(request);
} catch (CalloutException e) {
System.debug(
'Callout failed: ' +
e.getMessage()
);
}
HTTP Status Code vs Exception
There is an important difference between a callout exception and an HTTP error response.
For example, the external API may return:
HTTP 500
The HTTP request itself may have completed successfully. Therefore, your code should also inspect the HTTP status code.
Integer statusCode = response.getStatusCode();
if (statusCode >= 200 &&
statusCode < 300) {
// Success
} else {
// API returned an error
}
6. JSON Exception Handling
JSON processing is common in Salesforce integrations.
String jsonResponse =
'{"name":"John"}';
Map<String, Object> response =
(Map<String, Object>)
JSON.deserializeUntyped(jsonResponse);
If the external system sends invalid JSON, deserialization can fail.
try {
Map<String, Object> response =
(Map<String, Object>)
JSON.deserializeUntyped(jsonResponse);
} catch (JSONException e) {
System.debug(
'Invalid JSON: ' +
e.getMessage()
);
}
For production integrations, logging the response safely can make debugging much easier.
7. Custom Exception Classes
Apex allows developers to create custom exception classes.
public class ApplicationException
extends Exception {
}
We can then use the custom exception for business validation.
public static void validateApplication(
Decimal amount
) {
if (amount == null || amount <= 0) {
throw new ApplicationException(
'Application amount must be greater than zero.'
);
}
}
Custom exceptions are useful when the error represents a business rule rather than a platform error.
Real-Time Example: Loan Application
Let's take a simple real-world Salesforce scenario.
Suppose we have a Loan Application and the following rules:
- Customer must exist.
- Loan amount must be greater than zero.
- Customer must have a mobile number.
- External credit service should be available.
We could create a service like this:
public class LoanApplicationService {
public static void createApplication(
Id accountId,
Decimal loanAmount
) {
try {
Account acc = [
SELECT Id, Name, Phone
FROM Account
WHERE Id = :accountId
LIMIT 1
];
if (String.isBlank(acc.Phone)) {
throw new LoanApplicationException(
'Customer mobile number is required.'
);
}
if (loanAmount == null ||
loanAmount <= 0) {
throw new LoanApplicationException(
'Loan amount must be greater than zero.'
);
}
Loan_Application__c application =
new Loan_Application__c(
Account__c = acc.Id,
Loan_Amount__c = loanAmount
);
insert application;
} catch (LoanApplicationException e) {
throw e;
} catch (DmlException e) {
System.debug(
'DML Error: ' +
e.getMessage()
);
throw new LoanApplicationException(
'Unable to create loan application.'
);
} catch (Exception e) {
System.debug(
'Unexpected Error: ' +
e.getMessage()
);
throw new LoanApplicationException(
'Unexpected error occurred.'
);
}
}
public class LoanApplicationException
extends Exception {
}
}
Notice that business validation and technical exceptions are treated differently.
Exception Handling in LWC + Apex
This becomes especially important when Apex is called from a Lightning Web Component.
For example:
@AuraEnabled
public static Account getAccount(Id accountId) {
try {
return [
SELECT Id, Name
FROM Account
WHERE Id = :accountId
LIMIT 1
];
} catch (Exception e) {
throw new AuraHandledException(
'Unable to retrieve account details.'
);
}
}
The LWC can then handle the error:
import getAccount
from '@salesforce/apex/AccountController.getAccount';
getAccount({ accountId: this.recordId })
.then(result => {
this.account = result;
})
.catch(error => {
console.error(error);
this.showToast(
'Error',
error.body.message,
'error'
);
});
Why Use AuraHandledException?
It is commonly used when Apex needs to return a controlled error message to an Aura or LWC client.
throw new AuraHandledException(
'Customer mobile number is required.'
);
The user can see a meaningful message instead of a long technical stack trace.
Exception Logging
In a production Salesforce project, relying only on debug logs can become difficult.
A practical approach is to create a custom object such as:
Exception_Log__c
Possible fields include:
- Class_Name__c
- Method_Name__c
- Error_Message__c
- Exception_Type__c
- Line_Number__c
- Stack_Trace__c
- Record_Id__c
- Request_Body__c
- Response_Body__c
- Status__c
Example:
catch (Exception e) {
Exception_Log__c log =
new Exception_Log__c();
log.Class_Name__c =
'LoanApplicationService';
log.Method_Name__c =
'createApplication';
log.Error_Message__c =
e.getMessage();
log.Exception_Type__c =
e.getTypeName();
log.Line_Number__c =
e.getLineNumber();
log.Stack_Trace__c =
e.getStackTraceString();
insert log;
}
Transaction Rollback and Exception Logging
There is an important point that developers sometimes miss.
If your exception log record is inserted inside the same transaction that later rolls back, the log record can also be rolled back.
For important production logging, you need to think about transaction boundaries.
Depending on the architecture, teams may use:
- Platform Events
- Asynchronous processing
- Dedicated logging frameworks
- External monitoring systems
- Separate transaction patterns
The right solution depends on the project's requirements.
Can We Catch Governor Limit Exceptions?
This is a common Salesforce interview question.
You should not design your application assuming that a normal
try-catch can recover from a governor limit violation.
Instead, write code that respects Salesforce limits from the beginning.
For example, avoid SOQL inside a loop:
for (Account acc : accounts) {
List<Contact> contacts = [
SELECT Id
FROM Contact
WHERE AccountId = :acc.Id
];
}
A bulkified approach is:
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
accountIds.add(acc.Id);
}
List<Contact> contacts = [
SELECT Id, AccountId
FROM Contact
WHERE AccountId IN :accountIds
];
Database Methods and Partial Success
Sometimes we don't want one bad record to cause the entire operation to fail.
For this scenario, the Database methods are very useful.
Database.SaveResult[] results =
Database.insert(accounts, false);
The false parameter allows partial success.
for (Database.SaveResult result : results) {
if (result.isSuccess()) {
System.debug(
'Record inserted: ' +
result.getId()
);
} else {
for (Database.Error error :
result.getErrors()) {
System.debug(
'Error: ' +
error.getMessage()
);
}
}
}
This pattern is useful for:
- Data migration
- Integrations
- Batch processing
- Large data imports
- Bulk operations
Exception Handling in Queueable Apex
Queueable Apex is often used for asynchronous processing and integrations.
public class CustomerSyncQueueable
implements Queueable, Database.AllowsCallouts {
public void execute(
QueueableContext context
) {
try {
// Call external API
} catch (CalloutException e) {
System.debug(
'API Error: ' +
e.getMessage()
);
} catch (Exception e) {
System.debug(
'Unexpected Error: ' +
e.getMessage()
);
}
}
}
In a real production integration, simply using
System.debug() may not be enough.
You may need to:
- Log the failure.
- Store the integration status.
- Store useful request information safely.
- Store response information safely.
- Retry temporary failures where appropriate.
- Notify support when required.
Retry Logic: Should Every Error Be Retried?
No.
This is an important point in integration development.
For example, temporary errors such as:
HTTP 500
HTTP 502
HTTP 503
Timeout
may be temporary and could potentially be retried.
But errors such as:
HTTP 400
Invalid customer ID
Missing required field
Invalid business data
usually require the data or request to be corrected first.
Common Exception Handling Mistakes
1. Empty Catch Block
try {
insert account;
} catch (Exception e) {
}
Now the error has effectively disappeared.
2. Only Using System.debug()
catch (Exception e) {
System.debug(e.getMessage());
}
This may be fine during development, but important production errors often need persistent logging.
3. Showing Technical Details to Users
Avoid sending stack traces directly to an LWC.
throw new AuraHandledException(
e.getStackTraceString()
);
Instead, log the technical details and return a safe message.
System.debug(e.getStackTraceString());
throw new AuraHandledException(
'Something went wrong while processing your request.'
);
4. One Huge Try Block
Don't put an entire application inside one giant try block.
Keep methods focused so that errors can be understood and handled at the right level.
5. Using Exceptions for Normal Business Flow
If an Account doesn't exist and that is a valid business scenario, don't use an exception just to check whether the record exists.
Use normal validation and conditional logic where appropriate.
Salesforce Exception Handling Best Practices
1. Catch Specific Exceptions Where Useful
catch (DmlException e) {
// Handle DML problem
}
2. Log Important Production Failures
Debug logs are useful, but production applications may require a persistent logging mechanism.
3. Don't Expose Technical Information
Users need a useful business message, not a stack trace.
4. Keep Try Blocks Focused
Smaller blocks are easier to troubleshoot and maintain.
5. Validate Before DML
if (String.isBlank(customerName)) {
throw new ApplicationException(
'Customer name is required.'
);
}
6. Use Database Methods When Partial Success Is Required
Database.insert(records, false);
7. Design for Governor Limits
Don't expect exception handling to fix poor bulkification.
8. Handle Callout Failures Properly
Check both the actual callout exception and the HTTP status code returned by the external system.
9. Never Hide Exceptions
catch (Exception e) {
}
catch (Exception e) {
```
logException(e);
throw new AuraHandledException(
'Unable to process the request.'
);
```
}
10. Test Exception Scenarios
A good Apex test class should not test only the happy path.
Also test:
- Missing required fields
- Invalid IDs
- No records found
- DML failures
- Callout failures
- Invalid JSON
- Business validation failures
- Partial-success scenarios
Salesforce Exception Handling Interview Questions
Q1. What is Exception Handling in Apex?
Exception handling is a mechanism used to detect and handle runtime errors in
Apex using constructs such as try, catch,
throw, finally and custom exceptions.
Q2. What is the difference between DmlException and QueryException?
DmlException occurs when a DML operation fails, while
QueryException occurs when a SOQL operation encounters a runtime
query problem.
Q3. Can we rely on try-catch to handle governor limit violations?
No. The better approach is to design Apex using bulkification and governor-limit-aware coding practices.
Q4. What is AuraHandledException?
AuraHandledException is commonly used when Apex needs to return a
controlled error message to an Aura or LWC client.
Q5. What is the difference between insert and Database.insert()?
The insert statement performs a DML operation and throws a
DmlException when the operation fails.
Database.insert() provides additional control, including the option
to allow partial success.
Database.insert(
accounts,
false
);
Q6. Why do we create custom exceptions?
Custom exceptions allow developers to represent application-specific business errors clearly.
public class LoanValidationException
extends Exception {
}
Final Thoughts
Exception handling looks simple when you first learn Apex.
You learn:
try {
}
catch (Exception e) {
}
and it can feel like the topic is finished.
But in a real Salesforce project, exception handling is much more than that.
Your application needs to answer questions such as:
- What failed?
- Why did it fail?
- Which record was being processed?
- Can the operation be retried?
- Should the transaction continue?
- What should the user see?
- How will the support team troubleshoot it?
Don't use exception handling just to hide problems. Use it to understand, control and recover from problems.
If you are learning Salesforce development, don't just practice the happy path. Try deliberately creating errors in your Apex code and learn how Salesforce behaves. That's one of the fastest ways to become comfortable with exception handling.
Related Salesforce Topics
If you are learning Apex, the next topics worth exploring are:
Apex Governor Limits Apex Triggers Queueable Apex REST API LWC Salesforce Integration Apex Testing