SapotaCorp

DML Partial Success in Apex: Database.SaveResult and Savepoints

A bulk update on 5,000 records. Twelve fail validation. Should the other 4,988 commit, or should the whole batch roll back? The answer depends on the business case, and Apex offers three patterns to control it.

DML Partial Success in Apex: Database.SaveResult and Savepoints

Key takeaways

  • Bulk DML faces a decision: all-or-none (one failed record rolls back the batch) or partial success (failed records surface as errors, successful records commit). Apex offers 3 patterns to control it: insert with allOrNone, Database.insert with allOrNone flag, and savepoint+rollback for custom logic.
  • Default insert/update DML is all-or-none. The platform throws DmlException on the first failure and rolls back every record in the batch. Production code that needs partial success must use Database.insert with allOrNone=false.
  • Database.SaveResult inspection is the post-DML pattern. Iterate the SaveResult[] array, identify failed records via isSuccess(), log errors via getErrors(). The pattern lets business logic handle each failure individually (retry, notify, escalate) rather than failing the whole batch.
  • Savepoints provide finer-grained rollback control. Set a savepoint before risky DML; if the subsequent logic detects a problem, rollback to the savepoint and keep the pre-savepoint changes. Useful for multi-step transactional workflows where some steps can fail independently of others.

A bulk update processes 5,000 records. Validation rules reject 12 of them. The remaining 4,988 are clean. Should the 4,988 commit, or should the whole batch roll back?

The answer is business-specific. Sometimes partial success is correct (process what works, log the failures for manual review). Sometimes it is wrong (the records are interdependent and a partial commit leaves the data inconsistent). Apex offers three patterns to make the choice explicit.

The default behavior, when no pattern is chosen, is often the wrong default for production code.

The three Apex DML patterns

Pattern 1: simple DML keyword (insert, update, delete).

The all-or-nothing default. Any single record failing throws DmlException and rolls back the entire DML statement. The records that would have succeeded are not committed.

List<Account> accounts = buildLargeList();
insert accounts; // If any one fails, all roll back.

When this is right: when records are logically interdependent and partial commit would leave the org inconsistent.

When this is wrong: when records are independent and partial success is acceptable.

Pattern 2: Database method with allOrNone = true (explicit version of pattern 1).

Same behavior as the keyword, but the syntax surfaces the choice:

Database.SaveResult[] results = Database.insert(accounts, true);
// True = allOrNone. Throws if any fail.

Useful for clarity in code review. The true parameter announces "this is intentionally all-or-nothing." Future maintainers see the choice rather than inheriting an implicit default.

Pattern 3: Database method with allOrNone = false (partial success).

The DML continues even if some records fail. Returns results per record.

Database.SaveResult[] results = Database.insert(accounts, false);
// False = allow partial success.

for (Integer i = 0; i < results.size(); i++) {
  Database.SaveResult sr = results[i];
  if (!sr.isSuccess()) {
    // Inspect the errors.
    for (Database.Error err : sr.getErrors()) {
      System.debug('Record ' + i + ' failed: ' +
                    err.getStatusCode() + ': ' + err.getMessage());
    }
    // Log to a custom error log object, alert, etc.
  }
}

The shape: iterate the results array in the same order as the input list. Each result tells whether that record committed or failed, and why.

When this is right: bulk operations where partial completion is acceptable and failures are logged for manual review (e.g., a data import that processes 10,000 records and accepts 50 failures with reasons).

When this is wrong: any transaction where the records depend on each other or where downstream code assumes all-or-nothing semantics.

Savepoints for nested transaction control

For operations more complex than a single DML, Apex supports savepoints. A savepoint marks a transaction state that can be rolled back to.

Savepoint sp = Database.setSavepoint();
try {
  // Multiple DML operations.
  insert accounts;
  insert contacts;
  update opportunities;

  // Some logic that may throw.
  validateBusinessRules();

  // If we reach here, commit happens at the end of the transaction.
} catch (Exception e) {
  Database.rollback(sp);
  // All DML since the savepoint is undone.
  logError(e);
  throw e; // Or handle gracefully.
}

The savepoint behaves like a transaction nested inside the outer Apex transaction. Multiple operations succeed or fail together; the outer transaction continues with the post-rollback state.

When savepoints help: complex business logic that needs all-or-nothing semantics across multiple DML calls. The simple keyword can't roll back work done before the failing DML; savepoints can.

When savepoints have limits: only one savepoint per top-level transaction is fully reliable in practice. Nested savepoints behave differently than developers from RDBMS backgrounds expect. Test the rollback semantics in the specific context.

A practical decision tree

For every DML in production Apex code:

  1. Is this part of a transaction where records are logically interdependent? → Use the simple keyword or Database.insert(records, true).

  2. Is this a bulk operation where some failures are acceptable? → Use Database.insert(records, false). Process the SaveResult array. Log failures for review.

  3. Is this a multi-DML operation that should all succeed or all fail together? → Wrap in a try/catch with Database.setSavepoint() and Database.rollback().

  4. Is this hidden inside a generic helper used by many callers? → Make the allOrNone choice a parameter. Let each caller decide.

Common DML mistakes

Five patterns Sapota has seen in audits:

1. Plain insert records; for bulk data loads.

A nightly batch processes 50,000 records. One fails on validation. All 50,000 roll back. Tomorrow night the same record fails again. The job has been "running" for 2 months but committing nothing.

Fix: use Database.insert(records, false). Log failed records for review.

2. Database.insert without inspecting SaveResult.

The code uses Database.insert(records, false) (partial success). Then ignores the returned array. Failures are silently dropped.

Fix: iterate the SaveResult and log every failure to a tracking object or platform event.

3. Loop with individual DML.

// Anti-pattern: DML inside loop.
for (Account a : accounts) {
  try {
    update a;
  } catch (DmlException e) {
    // ... handle per-record ...
  }
}

This is the "per-record exception handling" pattern done wrong. It hits the 150-DML governor limit at 151 records. Use partial-success DML instead.

Fix: Database.update(accounts, false) plus SaveResult inspection.

4. Mixing allOrNone modes within one workflow.

Step 1 uses simple insert (all-or-nothing). Step 2 uses Database.insert(records, false) (partial). If step 1 fails, step 2 never runs. If step 2 has partial failures, the workflow is half-done. The mixed semantics produce hard-to-debug states.

Fix: pick one mode for the entire workflow. Use savepoints if you need atomic semantics across mixed operations.

5. Savepoint rollback without re-raising.

Savepoint sp = Database.setSavepoint();
try {
  // ... DML ...
} catch (Exception e) {
  Database.rollback(sp); // Rolled back.
  // No re-raise. Caller thinks the operation succeeded.
}

The caller has no way to know the operation failed. Downstream code may proceed expecting the records to exist.

Fix: after rollback, either re-throw the exception or return a clear failure indicator to the caller.

Inspecting Database.Error

The Database.Error returned by SaveResult has a few useful properties:

  • getStatusCode(): the standard status code (REQUIRED_FIELD_MISSING, FIELD_CUSTOM_VALIDATION_EXCEPTION, etc.).
  • getMessage(): a human-readable description.
  • getFields(): which fields contributed to the error (if applicable).

For logging, capture all three:

for (Database.SaveResult sr : results) {
  if (!sr.isSuccess()) {
    Error_Log__c log = new Error_Log__c(
      Record_Id__c = sr.getId(),
      Status_Code__c = String.valueOf(sr.getErrors()[0].getStatusCode()),
      Message__c = sr.getErrors()[0].getMessage(),
      Fields__c = String.join(sr.getErrors()[0].getFields(), ',')
    );
    insert log;
  }
}

The error log is the data set for triaging failures. Operations teams query it to see what is failing and why.

When DML is the wrong layer

Sometimes the right answer is not "different DML pattern" but "different architecture." Examples:

Bulk data imports. Use Bulk API, not Apex DML. The Bulk API is purpose-built for high-volume operations with native partial-success semantics. Apex DML is the wrong tool above 50K records.

Idempotent operations on individual records. Use upsert with an external ID. Lets the operation be safely retried without producing duplicates.

Distributed transactions across systems. Apex DML cannot roll back external system changes. Use a saga or 2-phase commit pattern across systems instead.

What good DML discipline looks like

A Salesforce org with healthy DML patterns:

  • Every DML call has a documented allOrNone intent.
  • Partial-success DML always inspects SaveResult and logs failures.
  • Multi-DML workflows use savepoints when atomic semantics are needed.
  • Bulk imports use Bulk API, not Apex DML.
  • Error_Log__c (or equivalent) captures every failure with context.
  • Code review explicitly checks the DML pattern selection.

Sapota's Salesforce team holds the Platform Developer I credential and treats DML pattern selection as a design decision on every engagement. The wrong choice is invisible until production hits a partial failure; the right choice handles those failures gracefully without losing data.


Designing bulk DML patterns or auditing existing Apex for partial-success handling? Sapota's Salesforce team handles DML architecture, error logging, and Bulk API integration on production engagements. Get in touch ->

See our full platform services for the stack we cover.

Contact Us Now

Share Your Story

We build trust by delivering what we promise – the first time and every time!

We'd love to hear your vision. Our IT experts will reach out to you during business hours to discuss making it happen.

WHY CHOOSE US

"Collaborate, Elevate, Celebrate where Associates - Create Project Excellence"

SapotaCorp beyond the IT industry standard, we are

  • Certificated
  • Assured quality
  • Extra maintenance

Tell us about your project