All core concepts from the Debouncing Guide apply to async debouncing as well.
You can usually just use the normal synchronous debouncer and it will work with async functions, but for advanced use cases, such as wanting to use the return value of a debounced function (instead of just calling a setState side effect), or putting your error handling logic in the debouncer, you can use the async debouncer.
TanStack Pacer provides async debouncing through the AsyncDebouncer class and the asyncDebounce function.
Here's a basic example showing how to use the async debouncer for a search operation:
const debouncedSearch = asyncDebounce(
async (searchTerm: string) => {
const results = await fetchSearchResults(searchTerm)
return results
},
{
wait: 500,
onSuccess: (results, args, debouncer) => {
console.log('Search succeeded:', results)
console.log('Search arguments:', args)
},
onError: (error, args, debouncer) => {
console.error('Search failed:', error)
console.log('Failed arguments:', args)
}
}
)
// Usage
try {
const results = await debouncedSearch('query')
// Handle successful results
} catch (error) {
// Handle errors if no onError handler was provided
console.error('Search failed:', error)
}Note: When using React, prefer useAsyncDebouncedCallback hook over the asyncDebounce function for better integration with React's lifecycle and automatic cleanup.
Unlike the synchronous debouncer which returns void, the async version allows you to capture and use the return value from your debounced function. The maybeExecute method returns a Promise that resolves with the function's return value, allowing you to await the result and handle it appropriately.
The async debouncer provides robust error handling capabilities:
If your debounced function throws an error and no onError handler is provided, the error will be thrown and propagate up to the caller
If you provide an onError handler, errors will be caught and passed to the handler instead of being thrown
The throwOnError option can be used to control error throwing behavior:
You can track error counts using debouncer.store.state.errorCount and check execution state with debouncer.store.state.isExecuting
The debouncer maintains its state and can continue to be used after an error occurs
The AsyncDebouncer supports the following callbacks:
onSuccess: Called after each successful execution, providing the result, the arguments that were executed, and debouncer instance
onSettled: Called after each execution (success or failure), providing the arguments that were executed and debouncer instance
onError: Called if the async function throws an error, providing the error, the arguments that caused the error, and the debouncer instance
Example:
const asyncDebouncer = new AsyncDebouncer(async (value) => {
await saveToAPI(value)
}, {
wait: 500,
onSuccess: (result, args, debouncer) => {
// Called after each successful execution
console.log('Async function executed', debouncer.store.state.successCount)
console.log('Executed arguments:', args)
},
onSettled: (args, debouncer) => {
// Called after each execution attempt
console.log('Async function settled', debouncer.store.state.settleCount)
console.log('Settled arguments:', args)
},
onError: (error) => {
// Called if the async function throws an error
console.error('Async function failed:', error)
}
})Since the debouncer's maybeExecute method returns a Promise, you can choose to await each execution before starting the next one. This gives you control over the execution order and ensures each call processes the most up-to-date data. This is particularly useful when dealing with operations that depend on the results of previous calls or when maintaining data consistency is critical.
For example, if you're updating a user's profile and then immediately fetching their updated data, you can await the update operation before starting the fetch.
The async debouncer includes built-in retry and abort capabilities through integration with AsyncRetryer. These features help handle transient failures and provide control over in-flight operations.
Configure automatic retries for failed debounced function executions using the asyncRetryerOptions:
const debouncedSave = asyncDebounce(
async (data: string) => {
// This might fail due to network issues
await api.save(data)
},
{
wait: 500,
asyncRetryerOptions: {
maxAttempts: 3,
backoff: 'exponential',
baseWait: 1000,
maxWait: 10000,
jitter: 0.3
}
}
)For complete documentation on retry strategies, backoff algorithms, jitter, and advanced retry patterns, see the Async Retrying Guide.
Cancel in-flight debounced executions using the abort functionality:
const debouncer = new AsyncDebouncer(
async (searchTerm: string) => {
// Access the abort signal for this execution
const signal = debouncer.getAbortSignal()
if (signal) {
const response = await fetch(`/api/search?q=${searchTerm}`, { signal })
return response.json()
}
},
{ wait: 300 }
)
// Start a search
debouncer.maybeExecute('query')
// Later, abort any in-flight execution
debouncer.abort()The abort functionality:
Cancels all ongoing debounced executions using AbortController
Does NOT cancel pending executions that haven't started yet (use cancel() for that)
Can be used alongside retry support
Abort only cancels underlying operations (e.g. fetch) when you pass the signal from getAbortSignal() to them. If your debounced function does not attach the signal, abort clears internal state but the async work continues until it completes.
For more details on abort patterns and integration with fetch/axios, see the Async Retrying Guide.
Use asyncDebouncerOptions to share common options between different AsyncDebouncer instances:
import { asyncDebouncerOptions, AsyncDebouncer } from '@tanstack/pacer'
const sharedOptions = asyncDebouncerOptions({
wait: 500,
leading: false,
trailing: true,
onSuccess: (result, args, debouncer) => console.log('Success')
})
const debouncer1 = new AsyncDebouncer(fn1, { ...sharedOptions, key: 'debouncer1' })
const debouncer2 = new AsyncDebouncer(fn2, { ...sharedOptions, onError: (error) => console.error('Error') })Just like the synchronous debouncer, the async debouncer supports dynamic options for wait and enabled, which can be functions that receive the debouncer instance. This allows for sophisticated, runtime-adaptive debouncing behavior.
The async debouncer supports flushing pending executions to trigger them immediately:
const asyncDebouncer = new AsyncDebouncer(asyncFn, { wait: 1000 })
asyncDebouncer.maybeExecute('some-arg')
console.log(asyncDebouncer.store.state.isPending) // true
// Flush immediately instead of waiting
asyncDebouncer.flush()
console.log(asyncDebouncer.store.state.isPending) // falseFramework hooks cancel any pending execution and abort any in-flight execution by default when a component unmounts. The automatic abort only cancels underlying operations (e.g. fetch) when the abort signal from getAbortSignal() is passed to them. Use the onUnmount option to flush instead of canceling.
const debouncer = useAsyncDebouncer(fn, {
wait: 500,
onUnmount: (d) => d.flush(),
})Warning: For async utils, flush() returns a Promise and runs fire-and-forget in the cleanup. If your debounced function updates React/Preact state or Solid signals, those updates may run after the component has unmounted, which can cause "setState on unmounted component" warnings or unexpected reactive updates. Guard your callbacks accordingly.
The AsyncDebouncer class uses TanStack Store for reactive state management, providing real-time access to execution state, error tracking, and execution statistics. All state is stored in a TanStack Store and can be accessed via asyncDebouncer.store.state, although, if you are using a framework adapter like React or Solid, you will not want to read the state from here. Instead, you will read the state from asyncDebouncer.state along with providing a selector callback as the 3rd argument to the useAsyncDebouncer hook to opt-in to state tracking as shown below.
Framework adapters support subscribing to state changes in two ways:
1. Using asyncDebouncer.Subscribe component (Recommended for component tree subscriptions)
Use the Subscribe component to subscribe to state changes deep in your component tree without needing to pass a selector to the hook. This is ideal when you want to subscribe to state in child components.
// Default behavior - no reactive state subscriptions at hook level
const asyncDebouncer = useAsyncDebouncer(asyncFn, { wait: 500 })
// Subscribe to state changes deep in component tree using Subscribe component
<asyncDebouncer.Subscribe selector={(state) => ({ isExecuting: state.isExecuting })}>
{(state) => (
<div>{state.isExecuting ? 'Executing...' : 'Idle'}</div>
)}
</asyncDebouncer.Subscribe>2. Using the selector parameter (For hook-level subscriptions)
The selector parameter allows you to specify which state changes will trigger reactive updates at the hook level, optimizing performance by preventing unnecessary updates when irrelevant state changes occur.
By default, asyncDebouncer.state is empty ({}) as the selector is empty by default. This is where the selected slice of TanStack Store state is exposed. You must opt-in to state tracking by providing a selector function.
// Default behavior - no reactive state subscriptions
const asyncDebouncer = useAsyncDebouncer(asyncFn, { wait: 500 })
console.log(asyncDebouncer.state) // {}
// Opt-in to re-render when isExecuting changes
const asyncDebouncer = useAsyncDebouncer(
asyncFn,
{ wait: 500 },
(state) => ({ isExecuting: state.isExecuting })
)
console.log(asyncDebouncer.state.isExecuting) // Reactive value
// Multiple state properties
const asyncDebouncer = useAsyncDebouncer(
asyncFn,
{ wait: 500 },
(state) => ({
isExecuting: state.isExecuting,
successCount: state.successCount,
errorCount: state.errorCount
})
)You can provide initial state values when creating an async debouncer. This is commonly used to restore state from persistent storage:
// Load initial state from localStorage
const savedState = localStorage.getItem('async-debouncer-state')
const initialState = savedState ? JSON.parse(savedState) : {}
const asyncDebouncer = new AsyncDebouncer(asyncFn, {
wait: 500,
initialState
})The store is reactive and supports subscriptions:
const asyncDebouncer = new AsyncDebouncer(asyncFn, { wait: 500 })
// Subscribe to state changes
const unsubscribe = asyncDebouncer.store.subscribe((state) => {
// do something with the state like persist it to localStorage
})
// Unsubscribe when done
unsubscribe()Note: Framework adapters already subscribe through useSelector (React/Preact/Solid) or injectSelector (Angular) from TanStack Store. To subscribe manually, use useSelector(store, selector, { compare: shallow }) from @tanstack/react-store (or @tanstack/preact-store / @tanstack/solid-store; import shallow from the same package), or injectSelector(store, selector) from @tanstack/angular-store.
The AsyncDebouncerState includes:
canLeadingExecute: Whether the debouncer can execute on the leading edge of the timeout
errorCount: Number of function executions that have resulted in errors
isExecuting: Whether the debounced function is currently executing asynchronously
isPending: Whether the debouncer is waiting for the timeout to trigger execution
lastArgs: The arguments from the most recent call to maybeExecute
lastResult: The result from the most recent successful function execution
maybeExecuteCount: Number of times maybeExecute has been called
settleCount: Number of function executions that have completed (either successfully or with errors)
status: Current execution status ('disabled' | 'idle' | 'pending' | 'executing' | 'settled')
successCount: Number of function executions that have completed successfully
Each framework adapter provides hooks that build on top of the core async debouncing functionality to integrate with the framework's state management system. Hooks like createAsyncDebouncer, useAsyncDebouncedCallback, or similar are available for each framework.
For core debouncing concepts and synchronous debouncing, see the Debouncing Guide.