How to implement debounce with abort in JavaScript
Debounce with abort capability combines delayed function execution with the ability to cancel pending operations, perfect for API calls that may become obsolete. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve implemented debounce with abort in production applications to optimize search and autocomplete features for millions of users.
The most effective approach combines traditional debounce with AbortController for async operation cancellation.
Basic Debounce with Abort
function debounceWithAbort(func, delay) {
let timeoutId
let controller
const debounced = (...args) => {
// Abort previous operation
if (controller) {
controller.abort()
}
// Clear previous timeout
clearTimeout(timeoutId)
// Create new AbortController
controller = new AbortController()
// Set new timeout
timeoutId = setTimeout(() => {
func(...args, controller.signal)
controller = null
}, delay)
}
debounced.cancel = () => {
clearTimeout(timeoutId)
if (controller) {
controller.abort()
controller = null
}
}
return debounced
}
Usage with Fetch API
const searchAPI = debounceWithAbort(async (query, signal) => {
try {
const response = await fetch(`/api/search?q=${query}`, { signal })
const data = await response.json()
console.log('Results:', data)
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request cancelled')
} else {
console.error('Search failed:', error)
}
}
}, 500)
// Usage in input handler
const input = document.getElementById('search')
input.addEventListener('input', (e) => {
searchAPI(e.target.value)
})
// Cancel on unmount or navigation
window.addEventListener('beforeunload', () => {
searchAPI.cancel()
})
Advanced Debounce with Abort
function createDebouncedAbortable(options = {}) {
const { delay = 300, leading = false, trailing = true } = options
return function debounce(func) {
let timeoutId
let controller
let lastCallTime = 0
const execute = (args, signal) => {
try {
return func(...args, signal)
} catch (error) {
if (error.name !== 'AbortError') {
throw error
}
}
}
const debounced = (...args) => {
const now = Date.now()
// Abort previous operation
if (controller) {
controller.abort()
}
controller = new AbortController()
// Leading edge execution
if (leading && now - lastCallTime > delay) {
lastCallTime = now
return execute(args, controller.signal)
}
// Clear previous timeout
clearTimeout(timeoutId)
// Trailing edge execution
if (trailing) {
timeoutId = setTimeout(() => {
lastCallTime = Date.now()
execute(args, controller.signal)
controller = null
}, delay)
}
}
debounced.cancel = () => {
clearTimeout(timeoutId)
if (controller) {
controller.abort()
controller = null
}
}
debounced.flush = () => {
if (timeoutId) {
clearTimeout(timeoutId)
lastCallTime = Date.now()
// Execute immediately without aborting
timeoutId = null
}
}
return debounced
}
}
Usage with Custom Options
const debouncedSearch = createDebouncedAbortable({
delay: 500,
leading: false,
trailing: true
})(async (query, signal) => {
const response = await fetch(`/api/search?q=${query}`, { signal })
return await response.json()
})
// Usage
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value)
})
Autocomplete Example
class Autocomplete {
constructor(input, resultsContainer) {
this.input = input
this.resultsContainer = resultsContainer
this.search = debounceWithAbort(async (query, signal) => {
if (query.length < 2) {
this.clearResults()
return
}
try {
const response = await fetch(`/api/autocomplete?q=${query}`, { signal })
const results = await response.json()
this.displayResults(results)
} catch (error) {
if (error.name === 'AbortError') {
console.log('Search cancelled')
} else {
console.error('Autocomplete error:', error)
}
}
}, 300)
this.input.addEventListener('input', (e) => {
this.search(e.target.value)
})
this.input.addEventListener('blur', () => {
this.search.cancel()
setTimeout(() => this.clearResults(), 200)
})
}
displayResults(results) {
this.resultsContainer.innerHTML = results
.map(item => `<div class="result">${item.name}</div>`)
.join('')
}
clearResults() {
this.resultsContainer.innerHTML = ''
}
}
const autocomplete = new Autocomplete(
document.getElementById('search'),
document.getElementById('results')
)
Multiple Concurrent Requests
Track and abort multiple operations:
function createAbortableDebouncer() {
const controllers = new Map()
return function debounce(func, delay, key = 'default') {
return (...args) => {
// Abort previous operation for this key
if (controllers.has(key)) {
controllers.get(key).abort()
}
const controller = new AbortController()
controllers.set(key, controller)
const timeoutId = setTimeout(() => {
func(...args, controller.signal)
controllers.delete(key)
}, delay)
return {
abort: () => {
clearTimeout(timeoutId)
controller.abort()
controllers.delete(key)
}
}
}
}
}
// Usage
const debouncer = createAbortableDebouncer()
const searchUsers = debouncer(async (query, signal) => {
const response = await fetch(`/api/users?q=${query}`, { signal })
return await response.json()
}, 500, 'users')
const searchPosts = debouncer(async (query, signal) => {
const response = await fetch(`/api/posts?q=${query}`, { signal })
return await response.json()
}, 500, 'posts')
// Both can run independently
searchUsers('john')
searchPosts('javascript')
React Hook Implementation
import { useRef, useCallback, useEffect } from 'react'
function useDebouncedAbort(callback, delay) {
const timeoutRef = useRef(null)
const controllerRef = useRef(null)
const debouncedCallback = useCallback(
(...args) => {
// Abort previous operation
if (controllerRef.current) {
controllerRef.current.abort()
}
// Clear previous timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Create new controller
controllerRef.current = new AbortController()
// Set new timeout
timeoutRef.current = setTimeout(() => {
callback(...args, controllerRef.current.signal)
controllerRef.current = null
}, delay)
},
[callback, delay]
)
const cancel = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
if (controllerRef.current) {
controllerRef.current.abort()
controllerRef.current = null
}
}, [])
// Cleanup on unmount
useEffect(() => {
return () => {
cancel()
}
}, [cancel])
return [debouncedCallback, cancel]
}
// Usage in component
function SearchComponent() {
const [results, setResults] = useState([])
const handleSearch = async (query, signal) => {
try {
const response = await fetch(`/api/search?q=${query}`, { signal })
const data = await response.json()
setResults(data)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Search failed:', error)
}
}
}
const [debouncedSearch, cancelSearch] = useDebouncedAbort(handleSearch, 500)
return (
<div>
<input
type="text"
onChange={(e) => debouncedSearch(e.target.value)}
onBlur={cancelSearch}
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
}
Promise-Based Debounce with Abort
function debounceAsync(func, delay) {
let timeoutId
let controller
let rejectPrevious
return (...args) => {
// Reject previous promise
if (rejectPrevious) {
rejectPrevious(new DOMException('Aborted', 'AbortError'))
}
// Abort previous operation
if (controller) {
controller.abort()
}
clearTimeout(timeoutId)
return new Promise((resolve, reject) => {
rejectPrevious = reject
controller = new AbortController()
timeoutId = setTimeout(async () => {
try {
const result = await func(...args, controller.signal)
resolve(result)
} catch (error) {
reject(error)
} finally {
rejectPrevious = null
controller = null
}
}, delay)
})
}
}
// Usage
const debouncedFetch = debounceAsync(async (url, signal) => {
const response = await fetch(url, { signal })
return await response.json()
}, 500)
// Returns promise
debouncedFetch('/api/data')
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request aborted')
}
})
Best Practice Note
This is the same debounce-with-abort pattern we use in CoreUI’s search and autocomplete components. Combining debounce with AbortController ensures optimal performance by preventing unnecessary API calls while also cancelling in-flight requests that are no longer needed. Always handle AbortError separately from other errors to avoid false error logging.
Related Articles
For related performance optimization, check out how to implement throttle with leading edge in JavaScript and how to create a memoization function in JavaScript.



