Log Entry
"Partial-Success Apex: Database Methods Done Properly"
Partial-success DML sounds simple:
Database.SaveResult[] results = Database.insert(records, false);
Set allOrNone to false, let the good records succeed, and handle the bad ones.
The dangerous bit is the final phrase.
If the code does not turn every SaveResult into a useful outcome, partial success becomes silent partial failure.
That is worse than a transaction that clearly failed.
Quick snapshot
Use partial-success DML when:
- records are independent units of work
- one invalid record should not block valid records
- the caller can understand item-level outcomes
- successful records do not need to be rolled back with failures
- retrying failed items is safe and deliberate
Do not use it when the collection represents one atomic business transaction.
My rule:
allOrNone=falseis not error handling. It is permission to receive more than one outcome. Your code still has to handle all of them.
Atomicity is a business requirement
Suppose a request creates an invoice header and five invoice lines.
If two lines fail, keeping the header and three lines may produce an invalid invoice. That operation should probably remain all-or-nothing.
Now suppose a nightly repair recalculates a derived value on 200 unrelated Accounts. One bad Account should not necessarily block the other 199.
That is a good partial-success candidate.
Choose based on whether the records form one invariant, not based on a desire to avoid catching DmlException.
Preserve the input-to-result relationship
Database.insert, update, and similar methods return results corresponding to the submitted record order.
Use the index to build an explicit outcome for the original input.
Database.SaveResult[] saveResults = Database.insert(accounts, false);
List<RecordOutcome> outcomes = new List<RecordOutcome>();
for (Integer index = 0; index < saveResults.size(); index++) {
Database.SaveResult saveResult = saveResults[index];
Account submittedAccount = accounts[index];
outcomes.add(
saveResult.isSuccess()
? RecordOutcome.succeeded(
submittedAccount.External_Key__c,
saveResult.getId()
)
: RecordOutcome.failed(
submittedAccount.External_Key__c,
toProblems(saveResult.getErrors())
)
);
}
Do not put the results into a set, sort them, or rebuild them from successful record IDs before correlation is complete.
The submitted record may not have an ID, so include a client key, source row number, external key, or another stable correlation value.
A result can contain more than one error
Do not read only the first error and discard the rest.
private static List<RecordProblem> toProblems(
Database.Error[] databaseErrors
) {
List<RecordProblem> problems = new List<RecordProblem>();
for (Database.Error databaseError : databaseErrors) {
RecordProblem problem = new RecordProblem();
problem.code = String.valueOf(databaseError.getStatusCode());
problem.message = databaseError.getMessage();
problem.fields = databaseError.getFields();
problems.add(problem);
}
return problems;
}
Field information is particularly useful for imports, admin tools, and LWCs that can identify the failed input.
Be careful about exposing raw database messages directly to end users. At an internal boundary they are valuable diagnostic material. At a public or customer-facing boundary they may need translation into controlled application messages.
Return a complete outcome model
I like results with a shape similar to:
public class RecordOutcome {
public String sourceKey;
public Boolean success;
public Id recordId;
public List<RecordProblem> problems;
}
Every submitted item gets exactly one RecordOutcome.
That makes the contract useful to:
- an invocable Apex action
- an LWC bulk editor
- a batch or Queueable job
- an import service
- a repair script
The caller should not need to infer failure because an ID is missing from a shorter success list.
Separate validation failures from DML failures
Some problems can be found before DML:
- missing source key
- unsupported operation
- duplicate request within the input
- mutually incompatible fields
Validate those first and create failed outcomes without sending obviously invalid records to DML.
Then run one collection DML operation for the remaining candidates and merge the results back into original order.
This produces better error codes and avoids consuming DML work for requests the application already knows are invalid.
Do not turn pre-validation into a duplicate implementation of every Salesforce validation rule. Database errors remain part of the contract.
Partial success does not isolate automation
Each submitted record can still invoke validation rules, Flows, triggers, roll-ups, and downstream automation.
A record can fail because of behaviour well beyond the class that called Database.update().
The successful records share the same broader transaction context. Governor limits are not reset for each item merely because their save results are separate.
Partial DML is not 200 tiny transactions.
Design the whole automation path for bulk execution.
Be deliberate about retries
A caller retrying the original collection can repeat records that already succeeded.
Safer options include:
- retry only failed outcomes
- use external IDs with
upsertwhere that matches the domain - assign a stable idempotency key to create operations
- record successful source keys durably
Never tell a user "try again" unless repeating the command has defined semantics.
Do not lose success through a later exception
Partial-success DML happens inside one Apex transaction.
If the code later throws an unhandled exception, the transaction can still roll back, including the records whose SaveResult reported success earlier in that transaction.
This surprises people.
Do not treat the intermediate SaveResult as a durable commit while the transaction is still running. Keep post-DML processing small and controlled, and understand what later work can invalidate the whole transaction.
If each unit truly needs an independent commit boundary, use an architecture with separate transactions rather than assuming allOrNone=false creates them.
Test mixed collections
The essential test contains both valid and invalid records.
Assert:
- valid records were saved
- invalid records were not saved
- every input has one outcome
- correlation keys are preserved
- all relevant errors are captured
- result ordering is correct
- retry logic selects only appropriate failures
Also test the atomic alternative for operations that must roll back together.
The decision is part of the behaviour and deserves a test.
The useful version of partial success
Partial-success DML is valuable when the caller receives a complete ledger:
- what was attempted
- what succeeded
- what failed
- why it failed
- what can safely happen next
Without that ledger, allOrNone=false merely makes failure quieter.
Quiet failure is not resilience.