MediumMicrosoftAmazonGoogleServicenowTikTokUberLinkedin

Debounce

Prompt

Implement a debounce function that takes a callback function and a delay in milliseconds, and returns a debounced version of it.

The returned function should forward all arguments to the original callback.

Playground

Hint 1

You need a way to cancel a previously scheduled function call when a new call comes in. JavaScript has a built-in pair of functions for scheduling and cancelling delayed execution.

Hint 2

setTimeout returns an ID. You can pass that ID to clearTimeout to cancel the scheduled execution. Store the ID somewhere that persists between calls (closure).

Hint 3

Each time the debounced function is called, clear the existing timeout (if any) and start a fresh one. The original function should only run inside the setTimeout callback.

Solution

Explanation

Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called.

When you type into a search box, you don't want to hit the API on every single keystroke. If someone types "javascript", that's 10 keystrokes and 10 API calls in under a second. Most of those results are useless because the user is still typing. What you actually want is to wait until they stop typing, and then make one API call with the final query.

That's debouncing. You delay execution until the caller has been quiet for a certain amount of time.

The solution

function debounce(fn, delay) {
let timeoutId;

return function (...args) {
// Save the context of 'this'
const context = this;

clearTimeout(timeoutId);

timeoutId = setTimeout(() => {
// Execute fn with the correct context and arguments
fn.apply(context, args);
}, delay);
};
}

The debounce function returns a new function. Every time that new function is called, it does two things:

  1. Cancels any existing timer with clearTimeout(timeoutId). If there was a previous call waiting to fire, it gets cancelled.
  2. Starts a fresh timer with setTimeout. The original function fn will run after delay milliseconds, but only if no new calls come in before then.

The timeoutId variable lives in a closure, so it persists across every call to the debounced function. Each call overwrites it with a new timer ID.

One really important detail here is how we save this into const context = this; and write fn.apply(context, args). You might be wondering, "Why not just call fn(...args)?" It's a great question! When you pass a function around, it can easily lose its this context. If someone uses your debounce on an object method or an event listener, this needs to point to the right place. By saving the context and using apply(context, args), we're saying, "Hey, run this function with the exact same this and arguments that were passed to the wrapper." It's a small detail, but interviewers love to see it because it shows you understand how tricky JavaScript context can be!

Why does only the last call execute?

Imagine calling debouncedLog three times rapidly:

  • debouncedLog('first'): sets a timer for 500ms
  • debouncedLog('second'): cancels the 'first' timer, sets a new timer for 500ms
  • debouncedLog('third'): cancels the 'second' timer, sets a new timer for 500ms

Now 500ms passes with no more calls. The timer from the third call fires, and log('third') runs. The first two calls never executed because their timers were cancelled before they could fire.

A real-world analogy

Think of an elevator. When someone presses the button, the doors don't close immediately. The elevator waits a few seconds. If another person walks up and presses the button again, the timer resets. The doors only close once nobody has pressed the button for the full waiting period.

You'll often see debounce used with search inputs, window resize handlers, and scroll event listeners. Any situation where an event fires rapidly but you only care about the final state.