TanStack
Guides

Angular Async Throttling Guide

Async throttling keeps the timing behavior described in the Throttling Guide, while adding Promise results, retries, error callbacks, and control over in-flight work.

Use it when a throttled operation returns a value you need, can reject, or needs retry and abort support. The synchronous throttling adapter can invoke an async function as a side effect, but it does not manage the resulting Promise.

Choose an API

  • injectAsyncThrottledCallback for a Promise-returning handler
  • injectAsyncThrottler for lifecycle methods and selected state

Angular example

ts
import { injectAsyncThrottler } from '@tanstack/angular-pacer'

export class SaveComponent {
  readonly saver = injectAsyncThrottler(
    savePosition,
    { wait: 1000 },
    (state) => ({
      isExecuting: state.isExecuting,
      isPending: state.isPending,
    }),
  )
}

The focused snippets later in this guide use injectAsyncThrottler and assume they run inside an Angular injection context.

Promise results

maybeExecute() returns a Promise. An immediate or trailing execution resolves with its result. When another call replaces pending trailing work, the older pending Promise resolves with the throttler's current lastResult:

plaintext
call A ─── execute A ─── result A
                  call B ───┐
                            ├─ call C replaces B ─── execute C
Promise B ──────────────────┘ resolves with result A
Promise C ────────────────────────────────────────── resolves with result C

The replaced call does not wait for the newer trailing execution. If every call needs its own execution and result, use an Async Queue.

An async throttler also avoids starting its next scheduled execution while the current execution is still active. The wait interval still controls throttle timing, while the Promise lifecycle can delay when later work is scheduled.

Leading and trailing execution

The edge combinations match synchronous throttling:

leadingtrailingBehavior
truetrueExecute immediately and retain the latest call for one trailing execution. This is the default.
truefalseExecute immediately and discard calls made during the interval.
falsetrueWait one interval before the first execution, then retain the latest call in each interval.
falsefalseRecord calls without executing the function.

Unlike debouncing, calls during the interval do not restart the interval. They only replace the pending trailing arguments.

Errors and callbacks

Async throttlers provide callbacks around each actual execution:

  • onSuccess(result, args, throttler) runs after success.
  • onError(error, args, throttler) runs after the retries for an execution fail.
  • onSettled(args, throttler) runs after either outcome.

Without onError, throwOnError defaults to true, so a failure rejects the Promise that owns the execution. Providing onError changes that default to false; the Promise then resolves with the current lastResult. Set throwOnError explicitly to override the default.

Callbacks describe executions, not every call to maybeExecute(). Replaced or discarded calls do not produce execution callbacks.

Retrying failed executions

Configure the retryer used for each execution with asyncRetryerOptions:

ts
const saver = injectAsyncThrottler(savePositionToServer, {
  wait: 1000,
  asyncRetryerOptions: {
    maxAttempts: 3,
    backoff: 'exponential',
    baseWait: 500,
    jitter: 0.2,
  },
})

maxAttempts includes the first attempt. Throttling controls logical executions; retrying controls the attempts within each execution. See the Async Retrying Guide before enabling retries for operations with side effects.

Canceling pending work and aborting active work

  • cancel() clears a pending trailing execution. It does not stop active work or reset the current throttle interval.
  • abort() aborts active executions. It does not clear pending trailing work.
  • flush() runs pending trailing work immediately and returns its result.

Pass the throttler's signal to the underlying API when it supports cancellation:

ts
const saver = injectAsyncThrottler(
  async (position: number) => {
    return fetch('/api/position', {
      method: 'POST',
      body: JSON.stringify({ position }),
      signal: saver.getAbortSignal() ?? undefined,
    })
  },
  { wait: 1000 },
)

saver.abort()

Calling abort() without using the signal stops retry management but cannot force an arbitrary Promise to stop.

Resetting safely

reset() restores default state, but it does not clear a scheduled timeout or guarantee that active work stops. Clean up the lifecycle first when necessary:

ts
saver.cancel()
saver.abort()
saver.reset()

Configuration

wait and enabled may be values or functions that receive the throttler instance. setOptions() merges new options into the existing configuration.

ts
saver.setOptions({
  enabled: (throttler) => throttler.store.state.errorCount < 3,
  wait: (throttler) => (throttler.store.state.successCount < 10 ? 500 : 1000),
})

A changed wait value does not reschedule existing trailing work. It applies to later scheduling and executions. Disabling the throttler through setOptions() cancels pending trailing work.

Use asyncThrottlerOptions() to define reusable, type-checked option objects.

Angular lifecycle

The adapter cancels pending work and aborts active work when its owner is destroyed. Providing onUnmount replaces that default cleanup, so a custom callback must perform every required lifecycle action. When custom cleanup flushes work, remember that user callbacks can run while the component is being destroyed.

Reactive state

The adapter subscribes only to the state returned by the selector argument. Without a selector, the adapter state is empty. Create the utility in an Angular injection context, usually as a component or service field initializer and select only fields used by the view:

ts
const throttler = injectAsyncThrottler(
  savePositionToServer,
  { wait: 1000 },
  (state) => ({
    isPending: state.isPending,
    isExecuting: state.isExecuting,
    lastResult: state.lastResult,
  }),
)

console.log(
  throttler.state().isPending,
  throttler.state().isExecuting,
  throttler.state().lastResult,
)

Option functions and lifecycle callbacks receive the underlying public utility instance. The .store.state reads inside those callbacks in the examples above are supported. Rendering code should read the selected adapter state shown here.

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.

  • isPending: Whether a trailing execution is scheduled.
  • isExecuting: Whether the wrapped function is active.
  • lastArgs: The latest arguments retained for trailing work.
  • lastResult: The most recent successful result.
  • lastExecutionTime and nextExecutionTime: Current timing boundaries.
  • successCount, errorCount, and settleCount: Execution outcome counts.

See the Angular API reference for adapter signatures and the public core reference for complete option and state types.