Community JavaScript Snippet
Debounce With Leading + Trailing Edges and a cancel() Method
The trailing-only debounce in every tutorial works for search inputs and breaks on click handlers. Here is the lodash-style version with leading edge, cancel(), and flush(), in 30 lines.
Debounce With Leading + Trailing Edges and a cancel() Method
The trailing-only debounce in every tutorial works for search inputs and breaks on click handlers. Here is the lodash-style version with leading edge, cancel(), and flush(), in 30 lines.
By @hiroshiward
December 18, 2025
·
Updated May 18, 2026
915 views
13
4.4 (8)
The four behaviors compose naturally on top of one timer reference. Leading is if (leading && timer === null), trailing is the contents of the timeout callback, cancel clears the timer and forgets the queued args, and flush clears the timer but invokes anyway with the queued args. The most common bug I have shipped in this code is forgetting to capture this and args together: a debounced method on a class instance must keep both for the fn.apply(ctx, args) call inside invoke, or you lose the this binding silently. The result-caching is a small lodash-compat detail that lets you read the previous return value off a debounced getter; few callers use it but its absence is surprising.
Leading-edge behavior is what makes a search input feel responsive: the spinner appears the moment the user types their first character, then suppresses every keystroke for the next 80ms, then fires again with the final value 80ms after the last keystroke. Trailing-only debounce makes the same input feel broken to a typing-speed user; the screen sits empty for almost a second between input and feedback. The cancel hook on this version is what I wire into a React unmount: useEffect(() => () => onChange.cancel(), []) so a leftover timer does not fire after the component is gone, which would normally produce a 'setState on unmounted component' warning.
Throttle and debounce sound similar but have opposite goals: debounce delays until quiet, throttle paces a stream. The cleanest version uses the timestamp of the last call rather than just a timer flag, because it tells you exactly how long until the next slot opens; that lets the trailing-call timer be set to wait - elapsed instead of wait, which preserves the regular cadence. I have used this exact throttle on a scroll handler that updates a sticky-header position; without it the handler fires 100 times per second and pegs the main thread. Without the trailing-arg capture, the very last scroll position before the user stops moving gets lost, which is a far more annoying bug than it sounds.
