# Vanilla Async Batching Guide

Async batching keeps the collection and trigger behavior described in the [Batching Guide](./batching.md), 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](./async-queuing.md) when each item needs its own execution or when you need to limit concurrency.

## How async batching works

Items collect until any configured trigger fires:

```text
add A ─── add B ─── add C
  │         │         │
  └─ wait reset       └─ maxSize reached
                            │
                            └─ execute [A, B, C]
```

A batch executes when:

- its length reaches `maxSize`,
- `getShouldExecute(items, batcher)` returns `true`, or
- no new item arrives for `wait` milliseconds.

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.

## Quick start

Use `asyncBatch` when adding items is the only operation you need:

```ts
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:

```ts
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()
```

## Promise results

`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:

- If that addition reaches `maxSize` or satisfies `getShouldExecute`, its Promise owns the triggered execution and resolves with the batch result.
- If it only schedules the wait timer, its Promise resolves without the later batch result.
- Another addition may reset the timer and change which items execute together.

Use `onSuccess`, `onError`, and `onSettled` when all additions need to observe the eventual batch outcome.

## Batch boundaries and overlapping work

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:

```text
execute [A, B] ───────────────── finish
       add C ─── add D ─── execute [C, D] ─── finish
```

If 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.

## Errors and failed items

Async batchers provide these callbacks:

- `onSuccess(result, batch, batcher)` after success.
- `onError(error, batch, batcher)` after the batch's retries fail.
- `onSettled(batch, batcher)` after either outcome.
- `onItemsChange(batcher)` when items are added or removed for execution.

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.

```ts
const failed = batcher.peekFailedItems()
for (const item of failed) {
  saveForManualRecovery(item)
}
```

For non-idempotent operations, verify the server outcome before resubmitting a failed batch.

## Retrying batches

Configure retries for each batch execution with `asyncRetryerOptions`:

```ts
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](./async-retrying.md) before retrying operations with side effects.

## Flushing, canceling, and clearing

- `flush()` clears the pending timer and immediately executes the current items.
- `cancel()` clears the pending timer but keeps the collected items.
- `clear()` removes collected items and failed items, but does not clear a scheduled timer.
- `peekAllItems()` returns a copy of the currently collected items.

Because `clear()` leaves the timer in place, use `cancel()` followed by `clear()` when no empty timer should remain:

```ts
batcher.cancel()
batcher.clear()
```

The batch function is not called when an eventual timer or `flush()` finds no items.

## Aborting active work

`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:

```ts
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.

### Resetting safely

`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:

```ts
batcher.cancel()
batcher.abort()
batcher.reset()
```

## Configuration and state

`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:

- `items`, `size`, and `isPending`: The next batch and its timer state.
- `isExecuting`: Whether a batch is reported as executing.
- `lastResult`: The most recent successful result.
- `failedItems` and `totalItemsFailed`: Failure tracking.
- `successCount`, `errorCount`, and `settleCount`: Batch outcome counts.
- `totalItemsProcessed`: Items in successful batch executions.

See the [`AsyncBatcher` API reference](../../../reference/classes/AsyncBatcher.md) for complete option and state types.
