Every Salesforce developer writes DML statements like insert, update, and delete. However, unexpected database rejections, permission restrictions, and governor limit boundaries can halt execution immediately if not handled defensively. Understanding the specific exceptions thrown during DML operations enables you to prevent transaction rollbacks and deliver actionable feedback to your users.
1. Understanding Common DML Exceptions
Salesforce surfaces distinct exception classes depending on what caused the database operation to fail:
DmlException: The standard exception thrown when a record fails database persistence due to validation rules, missing mandatory fields, duplicate detection rules, or failing Apex triggers.MixedDmlException: Thrown when modifying a Setup object (e.g.,User,Group,PermissionSetAssignment) and a Non-Setup standard/custom object (e.g.,Account,Contact) within the same synchronous transaction.NoAccessException/ Security Violations: Thrown when DML executes under strict user mode (as userorAccessLevel.USER_MODE) and the running user lacks Object-Level or Field-Level Security (FLS) permissions.LimitException: Raised when a transaction attempts more than 150 DML statements or modifies more than 10,000 total records in a single run.
- Total DML Statements: Maximum 150 per Apex transaction.
- Total Records Processed via DML: Maximum 10,000 records per transaction.
- All-or-Nothing Default: Standard statements (
insert records;) roll back the entire list if even a single record fails. - Partial Processing Option: Database methods (
Database.insert(records, false)) allow valid records to succeed while isolating failed rows.
2. Handling Hard Failures vs. Partial Processing
Salesforce offers two ways to execute DML: standalone statements that throw catchable exceptions, or Database class methods that allow partial success.
Using explicit savepoints to roll back transactions and extracting granular field-level failure messages:
public with sharing class AccountService {
public static void createAccounts(List<Account> accountsToInsert) {
Savepoint sp = Database.setSavepoint();
try {
insert as user accountsToInsert;
} catch (DmlException de) {
Database.rollback(sp);
for (Integer i = 0; i < de.getNumDml(); i++) {
System.debug(LoggingLevel.ERROR, 'Failed Row Index: ' + de.getDmlIndex(i));
System.debug(LoggingLevel.ERROR, 'Field: ' + de.getDmlFieldNames(i));
System.debug(LoggingLevel.ERROR, 'Message: ' + de.getDmlMessage(i));
}
throw new AuraHandledException('Unable to create accounts: ' + de.getDmlMessage(0));
}
}
}
Allowing valid records to save while logging individual failed rows without halting the thread:
public with sharing class BulkContactProcessor {
public static void updateContactsPartial(List<Contact> contactList) {
// Setting allOrNone parameter to false
Database.SaveResult[] results = Database.update(contactList, false, AccessLevel.USER_MODE);
for (Integer i = 0; i < results.size(); i++) {
Database.SaveResult sr = results[i];
if (sr.isSuccess()) {
System.debug(LoggingLevel.INFO, 'Successfully updated Contact ID: ' + sr.getId());
} else {
for (Database.Error err : sr.getErrors()) {
System.debug(LoggingLevel.ERROR, 'Error on record: ' + contactList[i].Id);
System.debug(LoggingLevel.ERROR, err.getStatusCode() + ': ' + err.getMessage());
System.debug(LoggingLevel.ERROR, 'Fields affected: ' + err.getFields());
}
}
}
}
}
3. Resolving the Mixed DML Trap
A frequent hurdle for developers is modifying both setup records (such as assigning a UserRole or inserting a User) and non-setup business records (such as creating an Account) in the same transaction.
Executing
insert newUser; followed immediately by insert newAccount; will throw a fatal System.MixedDmlException. This restriction prevents permission and sharing table corruption during transactional rollbacks.
In test classes, wrap setup DML inside
System.runAs(). In production logic, offload the setup or non-setup operation to a separate asynchronous job:
public with sharing class UserProvisioningService {
public static void onboardEmployee(String username, String accName) {
// 1. Create standard business record synchronously
Account acc = new Account(Name = accName);
insert as user acc;
// 2. Offload Setup Object modification asynchronously to avoid Mixed DML
assignRoleAsync(UserInfo.getUserId());
}
@future
public static void assignRoleAsync(Id targetUserId) {
UserRole devRole = [SELECT Id FROM UserRole WHERE DeveloperName = 'Engineering_Team' LIMIT 1];
User u = new User(Id = targetUserId, UserRoleId = devRole.Id);
update u;
}
}
4. Best Practices for Defensive DML Handling
Database.setSavepoint() before multi-object operations, and choose allOrNone=false when integrating batch pipelines.
- Inspect Granular Error Details: Use
de.getNumDml()andde.getDmlMessage(i)on caught exceptions instead of standardgetMessage()to identify the exact field and row index that caused the rejection. - Enforce User Mode Explicitly: Use
insert as userorAccessLevel.USER_MODEto respect Field-Level Security and sharing settings automatically. - Guard Against Limit Violations: Check remaining statement limits before executing heavy operations using
Limits.getDMLStatements()andLimits.getLimitDMLStatements(). - Surface Actionable UI Messages: Never let raw system exception stack traces reach user-facing components; transform caught exceptions into clean, localized
AuraHandledExceptionpayloads.
Summary
Handling DML exceptions defensively is vital for building reliable, production-ready Salesforce applications. By choosing between all-or-nothing transactions and partial Database methods, segregating setup objects to prevent Mixed DML errors, and extracting detailed error metadata, you ensure data consistency and deliver a resilient user experience.