Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

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()
})

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')
)

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

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 debounce a 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