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.

For related performance optimization, check out how to implement throttle with leading edge in JavaScript and how to create a memoization function in JavaScript.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
Open Source vs Commercial Admin Templates: Which Should You Choose in 2026?
Open Source vs Commercial Admin Templates: Which Should You Choose in 2026?

How to limit items in a .map loop in JavaScript
How to limit items in a .map loop in JavaScript

What is globalThis in JavaScript?
What is globalThis in JavaScript?

What is JavaScript Array.pop() Method?
What is JavaScript Array.pop() Method?

Answers by CoreUI Core Team