Error Summaries for Batch Operation Failures

Permalink to "Error Summaries for Batch Operation Failures"

Bulk operations rarely fail completely; they fail partially. An import of 12,400 rows adds 12,397 and rejects three. A bulk status change succeeds for 48 invoices and fails for two that are locked. The worst response is a generic “Some items could not be processed” toast. Users — especially screen reader users, who cannot scan a grid for red rows — need to know how many failed, which ones, why, and what they can do about it. That is an error summary, the same pattern form validation has used for years, applied to batch operations.

This guide belongs to notifications & toasts for data operations.

Spec reference

Permalink to "Spec reference"
  • SC 3.3.1 Error Identification (A) — failed items must be identified and the error described in text. For batch operations, that means each failed row and its reason.
  • SC 3.3.3 Error Suggestion (AA) — where a fix is known (“Date must be YYYY-MM-DD”), say so.
  • SC 4.1.3 Status Messages (AA) — the outcome counts are a status message if focus does not move; if the user is waiting for the result, moving focus to the summary is better.
  • SC 2.4.4 Link Purpose (A) — links to failed rows must say which row.
  • SC 1.3.1 Info and Relationships (A) — the summary is a list with a heading.
Error summary for a partially failed import Mock of a results table: three failed rows with row number, reference, field and reason; the summary heading above reads 12,397 added, 3 rejected. Error summary for a partially failed importRowInvoiceFieldReason18INV-0218Due dateNot a date: use…402INV-0602AmountNegative amounts are not…9,113INV-9313CustomerUnknown customer ID…Heading above the list: "Import finished: 12,397 added, 3 rejected". Retry rejected rows and Download rejected rowsfollow the list.
Each failure names the row, the field and what to fix.

When to use — and when not to

Permalink to "When to use — and when not to"

Use an error summary whenever a batch operation can partly fail and users need to act on the failures — imports, bulk edits, bulk deletions with locks, synchronisation with external systems.

Do not use a summary for single-item failures; show those in context. Do not list thousands of failures inline: show the first 20 with the total and offer a download of the full list.

The misapplication: marking failed rows in the grid with a red background and a toast saying “3 rows failed”. Screen reader users would have to visit every row to find them.

Toast plus red rows versus an error summary Comparison: a toast and red-highlighted rows are visual and transient; an error summary counts outcomes, lists failures with reasons and links, and offers follow-up actions. Toast plus red rows versus an error summary✗ Toast plus red rows"3 rows failed" then goneFailures found by scanning colourNo reason givenNo path to the rowNo follow-up✓ Error summary"12,397 added, 3 rejected" persistsFailures listed with reasonsField and fix for each failureLink to each affected rowRetry rejected, Download rejected

Annotated code example

Permalink to "Annotated code example"
<section class="error-summary" aria-labelledby="import-result">
  <!-- Focus target when the user waited for the result -->
  <h2 id="import-result" tabindex="-1">Import finished: 12,397 added, 3 rejected</h2>
  <p>Fix the rows below and retry, or download them to edit in a spreadsheet.</p>
  <ul>
    <!-- Each failure: which row, which field, what to do (SC 3.3.1, 3.3.3, 2.4.4) -->
    <li><a href="/imports/88/rows/18">Row 18, INV-0218</a>: Due date “31/02/2026” is not a date. Use YYYY-MM-DD.</li>
    <li><a href="/imports/88/rows/402">Row 402, INV-0602</a>: Amount −40.00 is negative. Amounts must be 0 or more.</li>
    <li><a href="/imports/88/rows/9113">Row 9,113, INV-9313</a>: Customer ID C-4471 does not exist.</li>
  </ul>
  <button type="button">Retry 3 rejected rows</button>
  <a href="/imports/88/rejected.csv" download>Download 3 rejected rows (CSV)</a>
</section>
function showBatchResult(result, { userWaiting }) {
  render(summaryFor(result));
  if (userWaiting) {
    document.getElementById('import-result').focus();      // user is waiting: move to the answer
  } else {
    notify({ text: `Import finished: ${result.ok} added, ${result.failed} rejected.`,
             kind: result.failed ? 'warning' : 'info',
             action: { label: 'View rejected rows', run: openSummary } });   // background: toast + history
  }
}

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected announcement AT deviations
Result while user waits Focus to heading: “Import finished: 12,397 added, 3 rejected, heading level 2” JAWS may continue reading the paragraph
Result in background Toast: “Import finished… 3 rejected.” with View action —
Tab through failures “Row 18, INV-0218, link” then reason text Reason read after the link in browse mode
Retry rejected “Retrying 3 rows” then a new summary —
Where the result goes Flow: was the user waiting on the page? If yes, render the summary and focus its heading. If not, show a toast with a View action and record it in history. In both cases the summary page keeps the full list. Where the result goesUser waiting on the page?import screen still open, no navigationyes: focus summary headingWorking elsewhere?navigated away or long jobtoast with "View rejected rows"Alwaysrecord in notification historylinks back to the summaryMany failures?show first 20 and the totalDownload all failures

Integration context

Permalink to "Integration context"

The summary is the destination of toasts from accessible toast notifications for background jobs. Per-cell errors inside an editable grid use inline form validation inside editable table cells. Bulk operations started from a selection come from contextual bulk action toolbars, whose announcements should hand off to this summary.

Gotchas

Permalink to "Gotchas"

Row numbers that change. “Row 18” in the uploaded file is not row 18 in the grid. Use the file’s row number and the record’s identifier together.

Reasons in codes. “E_FK_VIOLATION” is not a reason. Map errors to sentences users can act on.

Selection after partial failure. For bulk actions on a selection, keep the failed rows selected and deselect the succeeded ones, then say so: “2 invoices could not be archived and are still selected.”

Framework notes

Permalink to "Framework notes"

Server responses should return per-item results, not a single status. A common shape is { succeeded: [...ids], failed: [{ id, row, field, code, message }] }. In React with TanStack Query, render the summary from the mutation’s data in onSuccess, even when some items failed — partial failure is a successful response with failures in it, not an exception. In Angular, map the HTTP 207 Multi-Status responses some APIs use to the same shape.

// Partial failure is data, not an error
const { mutate } = useMutation({ mutationFn: bulkArchive, onSuccess: (res) => showBatchResult(res, { userWaiting: true }) });

Worked example

Permalink to "Worked example"

A product manager imports 2,000 SKUs from a supplier spreadsheet. Seven rows fail. Previously, the grid highlighted them in pink and a toast said “Import completed with errors.” Now, because she is waiting on the import screen, focus moves to a heading — “Import finished: 1,993 added, 7 rejected” — followed by a list where each item reads like “Row 112, SKU AB-2291: Price ‘12,50’ uses a comma. Use a full stop for decimals.” She downloads the seven rows, fixes them in her spreadsheet and uses “Retry 7 rejected rows”. The second summary reads “Import finished: 7 added, 0 rejected”, and the rows she fixed are now in the grid.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Add a BatchResult component that takes the per-item result shape and renders the summary, links and follow-up actions, plus a matching toast template. Document the error message style guide — row, field, problem, fix — next to it.

FAQ

Permalink to "FAQ"
Should failures be announced one by one?

No. Announce or focus the counts; users then read the list at their own pace.

Should focus always move to the summary?

Only when the user is waiting for the result on the same page. If they have moved on, a toast with a link is less disruptive.

How many failures should be listed inline?

Up to about 20. Beyond that, show the count, the first few, and a download of the full list.

Is partial success a success or an error?

Both: report it as a warning-level outcome with counts. Treating it as a plain success hides the failures; treating it as an error hides the work that succeeded.

Permalink to "Related"

← Back to Notifications & Toasts for Data Operations