Async batching keeps the collection and trigger behavior described in the Batching Guide, while adding Promise results, retries, error callbacks, failed-item tracking, and control over in-flight work.
Use it when one async operation should process several collected items together. Use an Async Queue when each item needs its own execution or when you need to limit concurrency.
Items collect until any configured trigger fires:
add A ─── add B ─── add C
│ │ │
└─ wait reset └─ maxSize reached
│
└─ execute [A, B, C]A batch executes when:
Both maxSize and wait default to Infinity, so configure at least one trigger or call flush() manually. The wait timer restarts on every addition. It measures a quiet period, not a maximum age for the oldest item.
Use asyncBatch when adding items is the only operation you need:
import { asyncBatch } from '@tanstack/pacer'
const addAnalyticsEvent = asyncBatch(
async (events: Array<AnalyticsEvent>) => {
const response = await fetch('/api/analytics/batch', {
method: 'POST',
body: JSON.stringify(events),
})
if (!response.ok) throw new Error('Batch failed')
return response.json()
},
{ maxSize: 20, wait: 1000 },
)
addAnalyticsEvent(event)Use AsyncBatcher for lifecycle methods, callbacks, and state:
import { AsyncBatcher } from '@tanstack/pacer'
const batcher = new AsyncBatcher(sendEvents, {
maxSize: 20,
wait: 1000,
onSuccess: (result, batch) => {
console.log('Sent:', batch.length, result)
},
onError: (error, batch) => {
console.error('Failed batch:', batch, error)
},
})
batcher.addItem(event)
const result = await batcher.flush()flush() returns the batch function's result and is the clearest way to await a specific batch.
addItem() also returns a Promise, but it should not be treated as an individual item's result receipt:
Use onSuccess, onError, and onSettled when all additions need to observe the eventual batch outcome.
The batcher copies and clears the current items before calling the async function. Items added while that function is active collect in a new batch:
execute [A, B] ───────────────── finish
add C ─── add D ─── execute [C, D] ─── finishIf the second batch's trigger fires before the first finishes, both batch functions can overlap. AsyncBatcher does not have a concurrency option. Serialize batch executions outside the batcher or send the completed batches through an async queue when overlap is unsafe.
Async batchers provide these callbacks:
Without onError, throwOnError defaults to true, so flush() or a size-triggering addItem() rejects on failure. Providing onError changes that default to false; the Promise then resolves with undefined.
Items are removed from the pending collection before execution. A failed batch is not automatically requeued. Its items are added to failedItems and are available through peekFailedItems() until clear() or a later execution clears that collection.
const failed = batcher.peekFailedItems()
for (const item of failed) {
saveForManualRecovery(item)
}For non-idempotent operations, verify the server outcome before resubmitting a failed batch.
Configure retries for each batch execution with asyncRetryerOptions:
const batcher = new AsyncBatcher(sendEvents, {
maxSize: 20,
wait: 1000,
asyncRetryerOptions: {
maxAttempts: 3,
backoff: 'exponential',
baseWait: 500,
jitter: 0.2,
},
})maxAttempts includes the first attempt, and every retry receives the same copied batch. See the Async Retrying Guide before retrying operations with side effects.
Because clear() leaves the timer in place, use cancel() followed by clear() when no empty timer should remain:
batcher.cancel()
batcher.clear()The batch function is not called when an eventual timer or flush() finds no items.
abort() aborts active retryers. It does not cancel a pending batch or remove collected items. Pass the batcher's signal to the underlying API for cancellation to propagate:
const batcher = new AsyncBatcher(
async (events: Array<AnalyticsEvent>) => {
return fetch('/api/analytics/batch', {
method: 'POST',
body: JSON.stringify(events),
signal: batcher.getAbortSignal() ?? undefined,
})
},
{ maxSize: 20, wait: 1000 },
)
batcher.abort()When executions overlap, pass an executeCount to getAbortSignal() when you need a specific execution's signal.
reset() restores default state, but it does not clear a scheduled timer or guarantee that active underlying work stops. Use the lifecycle methods first when a complete cleanup is required:
batcher.cancel()
batcher.abort()
batcher.reset()wait may be a number or a function that receives the batcher instance. setOptions() merges new options, and asyncBatcherOptions() creates reusable, type-checked option objects.
Do not use started to pause a batcher. It is currently a no-op, so every addItem() call evaluates the configured triggers.
To restore selected state that your app has persisted, pass a partial snapshot through initialState. It is merged with the defaults. Restore only durable fields. Pending timers and active executions are not restored.
Common state includes:
See the AsyncBatcher API reference for complete option and state types.