frontend
Debounce with Cancel in JavaScript
January 24, 2026
Debounce with Cancel in JavaScript
Overview
Debounce with cancel is an enhanced version of the debounce pattern that allows you to cancel pending function executions. This is useful when you need to abort debounced operations, clean up pending operations, or reset the debounce timer programmatically.
Basic Implementation
// Debounce with cancel delayed invocations
function debounceWithCancel(func, delay) {
let timeoutId;
function debounced(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
}
debounced.cancel = function () {
clearTimeout(timeoutId);
};
return debounced;
}
Usage Example
const debouncedSearch = debounceWithCancel((query) => {
console.log('Searching for:', query);
// Perform search
}, 300);
// User types
debouncedSearch('hello');
debouncedSearch('world');
debouncedSearch('javascript');
// Cancel before execution
debouncedSearch.cancel();
Advanced Implementation
With Immediate Option
function debounceWithCancel(func, delay, immediate = false) {
let timeoutId;
let lastArgs;
let lastThis;
function debounced(...args) {
lastArgs = args;
lastThis = this;
const callNow = immediate && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!immediate) {
func.apply(lastThis, lastArgs);
}
}, delay);
if (callNow) {
func.apply(lastThis, lastArgs);
}
}
debounced.cancel = function () {
clearTimeout(timeoutId);
timeoutId = null;
lastArgs = null;
lastThis = null;
};
debounced.flush = function () {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
func.apply(lastThis, lastArgs);
}
};
debounced.pending = function () {
return timeoutId !== null;
};
return debounced;
}
Complete Implementation with All Features
function debounceAdvanced(func, delay, options = {}) {
const {
immediate = false,
maxWait = null
} = options;
let timeoutId;
let maxTimeoutId;
let lastArgs;
let lastThis;
let lastCallTime;
let result;
function invokeFunc() {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = null;
result = func.apply(thisArg, args);
return result;
}
function leadingEdge() {
// Reset any `maxWait` timer
if (maxTimeoutId) {
clearTimeout(maxTimeoutId);
maxTimeoutId = null;
}
// Invoke the function
return invokeFunc();
}
function remainingWait() {
const timeSinceLastCall = Date.now() - lastCallTime;
const timeWaiting = delay - timeSinceLastCall;
return timeWaiting;
}
function shouldInvoke() {
const timeSinceLastCall = Date.now() - lastCallTime;
return lastCallTime === null || timeSinceLastCall >= delay;
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke()) {
return trailingEdge();
}
// Restart the timer
timeoutId = setTimeout(timerExpired, remainingWait());
}
function trailingEdge() {
timeoutId = null;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once
if (lastArgs) {
return invokeFunc();
}
lastArgs = lastThis = null;
return result;
}
function cancel() {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (maxTimeoutId) {
clearTimeout(maxTimeoutId);
}
lastCallTime = 0;
lastArgs = lastThis = timeoutId = maxTimeoutId = null;
}
function flush() {
return timeoutId === null ? result : trailingEdge();
}
function pending() {
return timeoutId !== null;
}
function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke();
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timeoutId === null) {
return leadingEdge();
}
if (maxWait) {
// Handle `maxWait`
timeoutId = setTimeout(timerExpired, delay);
return invokeFunc();
}
}
if (timeoutId === null) {
timeoutId = setTimeout(timerExpired, delay);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
debounced.pending = pending;
return debounced;
}
Use Cases
1. Search Input with Cancel
const searchInput = document.getElementById('search');
let debouncedSearch;
function performSearch(query) {
console.log('Searching for:', query);
// API call
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => displayResults(data));
}
debouncedSearch = debounceWithCancel(performSearch, 500);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Cancel search when input is cleared
searchInput.addEventListener('focusout', () => {
if (searchInput.value === '') {
debouncedSearch.cancel();
}
});
2. Auto-save with Cancel
const saveButton = document.getElementById('save');
const cancelButton = document.getElementById('cancel');
const autoSave = debounceWithCancel((data) => {
fetch('/api/save', {
method: 'POST',
body: JSON.stringify(data)
});
}, 2000);
// Auto-save on input
document.getElementById('editor').addEventListener('input', (e) => {
autoSave(e.target.value);
});
// Cancel auto-save on manual save
saveButton.addEventListener('click', () => {
autoSave.cancel();
// Manual save
});
// Cancel on cancel button
cancelButton.addEventListener('click', () => {
autoSave.cancel();
});
3. Resize Handler with Cancel
const handleResize = debounceWithCancel(() => {
console.log('Window resized');
// Update layout
updateLayout();
}, 250);
window.addEventListener('resize', handleResize);
// Cancel on page unload
window.addEventListener('beforeunload', () => {
handleResize.cancel();
});
4. Form Validation with Cancel
const validateForm = debounceWithCancel((formData) => {
// Perform validation
const errors = validate(formData);
displayErrors(errors);
}, 300);
form.addEventListener('input', () => {
validateForm(getFormData());
});
// Cancel validation on form submit
form.addEventListener('submit', (e) => {
e.preventDefault();
validateForm.cancel();
// Immediate validation
const errors = validate(getFormData());
if (errors.length === 0) {
submitForm();
}
});
React Hook Example
import { useEffect, useRef } from 'react';
function useDebounceWithCancel(callback, delay) {
const timeoutRef = useRef(null);
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const debounced = useRef((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callbackRef.current(...args);
}, delay);
}).current;
debounced.cancel = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
useEffect(() => {
return () => {
debounced.cancel();
};
}, []);
return debounced;
}
// Usage
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedSearch = useDebounceWithCancel((q) => {
performSearch(q);
}, 500);
const handleChange = (e) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
};
const handleCancel = () => {
debouncedSearch.cancel();
};
return (
<div>
<input value={query} onChange={handleChange} />
<button onClick={handleCancel}>Cancel</button>
</div>
);
}
Class-Based Implementation
class Debouncer {
constructor(func, delay, options = {}) {
this.func = func;
this.delay = delay;
this.immediate = options.immediate || false;
this.timeoutId = null;
this.lastArgs = null;
this.lastThis = null;
}
debounce(...args) {
this.lastArgs = args;
this.lastThis = this;
const callNow = this.immediate && !this.timeoutId;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(() => {
this.timeoutId = null;
if (!this.immediate) {
this.func.apply(this.lastThis, this.lastArgs);
}
}, this.delay);
if (callNow) {
this.func.apply(this.lastThis, this.lastArgs);
}
}
cancel() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
this.lastArgs = null;
this.lastThis = null;
}
flush() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
this.func.apply(this.lastThis, this.lastArgs);
}
}
pending() {
return this.timeoutId !== null;
}
}
Best Practices
- Always Cancel on Cleanup: Cancel debounced functions in cleanup/unmount
- Handle Pending State: Check if function is pending before canceling
- Memory Management: Clear references when canceling
- Error Handling: Handle errors in debounced functions
- Testing: Test cancel functionality thoroughly
Comparison with Regular Debounce
Regular Debounce
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// No way to cancel
Debounce with Cancel
function debounceWithCancel(func, delay) {
let timeoutId;
function debounced(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
}
debounced.cancel = () => clearTimeout(timeoutId);
return debounced;
}
// Can cancel: debounced.cancel()
Real-World Example
class SearchManager {
constructor() {
this.searchFunction = debounceWithCancel(this.performSearch.bind(this), 500);
}
performSearch(query) {
console.log('Searching:', query);
// API call
}
handleInput(query) {
this.searchFunction(query);
}
cancel() {
this.searchFunction.cancel();
}
destroy() {
this.cancel();
}
}
// Usage
const searchManager = new SearchManager();
searchInput.addEventListener('input', (e) => {
searchManager.handleInput(e.target.value);
});
// Cleanup
window.addEventListener('beforeunload', () => {
searchManager.destroy();
});