If a Batch job processes 1,000 records in chunks of 200 and the fifth (final) execute() fails on its DML, the first four chunks remain committed — they were each their own transaction, and each one ended with a successful commit. Only the failed chunk rolls back. This is fundamental to how Batch Apex works: each execute() is an independent transaction, not a piece of one big transaction.
The timeline
Chunk 1 (records 1–200): execute() runs → DML succeeds → COMMITTED
Chunk 2 (records 201–400): execute() runs → DML succeeds → COMMITTED
Chunk 3 (records 401–600): execute() runs → DML succeeds → COMMITTED
Chunk 4 (records 601–800): execute() runs → DML succeeds → COMMITTED
Chunk 5 (records 801–1000): execute() runs → DML fails → ROLLED BACK
finish() runs → COMMITTED (independently)
After the job ends:
- Records 1–800 carry whatever changes the batch made (persisted).
- Records 801–1000 are unchanged — that chunk’s transaction rolled back.
NumberOfErrorsonAsyncApexJobreflects 1 failed chunk.- The Apex Jobs UI shows the job as “Completed” (not “Failed”) if at least one execute succeeded.
Why Salesforce designed it this way
Batch is meant for high-volume work where a single bad row shouldn’t waste the work already done. If the entire job rolled back on any failure, processing 50 million records would be all-or-nothing — one timeout or one bad row late in the job would force a complete restart.
The trade-off is that you have to handle partial success deliberately:
- Log which chunks failed so you can re-process them.
- Don’t assume the database is in a fully consistent state mid-job.
- If you need transactional all-or-nothing semantics, you’ve picked the wrong tool — use Queueable in one transaction instead.
Tracking what failed
Use Database.Stateful to accumulate error info across chunks:
public class MyBatch implements Database.Batchable<sObject>, Database.Stateful {
private List<Id> failedChunkFirstIds = new List<Id>();
public void execute(Database.BatchableContext ctx, List<sObject> scope) {
Database.SaveResult[] results = Database.update(scope, false); // partial-success DML
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) failedChunkFirstIds.add(scope[i].Id);
}
}
public void finish(Database.BatchableContext ctx) {
// Email the failed Ids, write them to a custom log object, queue a retry
}
}
Pair Database.update(scope, false) with SaveResult inspection to capture errors per record rather than letting the whole chunk fail.
Catching the exception ≠ saving the chunk
Wrapping the DML in try/catch lets you log the failure, but the transaction has already rolled back by the time you catch. The chunk’s DML is gone. The only way to “save” half a chunk is to break it into smaller writes with explicit savepoints, which is rarely worth the complexity.
What finish sees
finish runs in its own transaction regardless of how many chunks succeeded. You can:
- Query
AsyncApexJobviaBatchableContext.getJobId()to get final statistics. - Send a completion email — including failure counts.
- Chain a retry batch for the failed records.
Common interview follow-ups
- Will the whole job stop after a failed chunk? — No. The next chunk continues. Batch processes all chunks unless explicitly aborted.
- How do I make it all-or-nothing? — You don’t, with Batch. Use Queueable, or accept that Batch is partial-success.
- What if
startfails? — Whole job fails. Nothing has run yet, so there’s nothing to roll back. - What if
finishfails? —finishis its own transaction. Chunks remain committed. The job is logged with an error.
Verified against: Apex Developer Guide — Using Batch Apex, Apex Transaction Control. Last reviewed 2026-05-17.