Medium

Throttle Event Handler

Prompt

/**
* We have a function called makeRequest that makes an API request.
* We also have a function called eventHandler that is hooked up to
* an event (a click, for example), and right now it's a dumb handler
* that simply calls makeRequest.
*
* Write a new event handler eventHandlerWithThrottling that we can hook
* up to the click event such that it won't call makeRequest more
* than once every 5 seconds.
*
* We do not care what the API request returns (assume it's a fire-and-forgot POST)
*
* Events that occur within 5 seconds of an API request are "remembered",
* and once the 5 second timeout is over, **the latest one will be fired**.
**/

let now = 0;

function makeRequest(payload) {
// let's pretend this makes a request, and logs the payload/time of request
console.log({
data: payload,
time: Math.round((new Date().getTime() - now) / 1000)
});
}

const throttle = (callback, delay) => (payload) => callback(payload);
// const throttledMakeRequest = throttle(makeRequest, 5000);

export default throttle;

Playground

Hint 1

You need to track two things: when the last call was made, and whether there's a pending event waiting to fire. Think about what variables you'd need to store this state using closure.

Hint 2

When a new event comes in, check if enough time has passed since the last call. If yes, fire immediately. If not, you need to "remember" this event by scheduling it for later using setTimeout. Since only the latest event matters, clear any previously scheduled timeout before setting a new one.

Hint 3

Calculate how much time is left before you're allowed to make the next call. Use Date.now() to track timestamps, and schedule the remembered event to fire after the remaining time with setTimeout. Don't forget to update the last call timestamp when the scheduled event fires too!

Solution

Explanation

This question tests your understanding of throttling with a twist — not only do you need to limit how often a function runs, but you also need to remember the latest event and fire it once the cooldown period is over. Let's break it down.

What Makes This Different from Basic Throttle?

A basic throttle simply ignores all calls during the cooldown period. This version is smarter — it remembers the most recent call and fires it after the cooldown. This means no events are silently lost.

Think of it like an elevator door: if you press the button while the door is closing, it won't open immediately, but it will remember your request and open once it's safe to do so.

The Closure Variables

We use closure to keep track of state between calls:

let lastCallTime = 0;
let timeoutId = null;
  • lastCallTime tracks when we last actually called the callback. Starting at 0 means the very first call will always fire immediately.
  • timeoutId stores the ID of any scheduled future call, so we can cancel it if a newer event comes in.

Handling Each Event

When the returned function is called, we first figure out how much time has passed:

const now = Date.now();
const timeSinceLastCall = now - lastCallTime;

Then we clear any previously scheduled call. This is the key to "remembering only the latest" — if we already scheduled an older event, we throw it away in favor of the new one:

if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}

The Decision: Fire Now or Later?

If enough time has passed since the last call, we fire immediately:

if (timeSinceLastCall >= delay) {
lastCallTime = now;
callback(payload);
}

Otherwise, we calculate how much time is left in the cooldown and schedule the call for later:

const remainingTime = delay - timeSinceLastCall;
timeoutId = setTimeout(() => {
lastCallTime = Date.now();
timeoutId = null;
callback(payload);
}, remainingTime);

Notice we update lastCallTime inside the timeout too — this ensures the cooldown resets after the scheduled call fires.

Dry Run

Let's trace through a scenario with a 5-second delay:

TimeEventWhat Happens
0sthrottledMakeRequest("click-1")timeSinceLastCall is huge (first call). Fires immediately. lastCallTime = 0s.
1sthrottledMakeRequest("click-2")Only 1s passed. Clears nothing (no pending timeout). Schedules "click-2" for 4s later.
2sthrottledMakeRequest("click-3")Only 2s passed. Clears the "click-2" timeout. Schedules "click-3" for 3s later.
5s(timeout fires)"click-3" fires. lastCallTime = 5s. timeoutId = null.
6sthrottledMakeRequest("click-4")1s since last call. Schedules "click-4" for 4s later.
7sthrottledMakeRequest("click-5")2s since last call. Clears "click-4" timeout. Schedules "click-5" for 3s later.
8sthrottledMakeRequest("click-6")3s since last call. Clears "click-5" timeout. Schedules "click-6" for 2s later.
10s(timeout fires)"click-6" fires. lastCallTime = 10s.

Notice how "click-2" and "click-5" never fire — they were replaced by newer events. Only the latest event in each window gets executed.

Notes

Throttle vs Debounce

This trailing throttle might remind you of debounce, but they're different:

  • Debounce resets the timer on every call, so the function only fires after you stop calling it for the full delay.
  • Trailing Throttle fires at most once per delay window, and the timer is based on the last actual execution — not the last call.

In this problem, if you click at 0s and then click at 1s, the trailing throttle fires the first click immediately and schedules the second for 5s after the first. A debounce would reset and wait 5s from the second click.

Why Clear the Previous Timeout?

Since we only care about the latest event during the cooldown, we must clear any previously scheduled timeout before setting a new one. Without this, multiple events could queue up and all fire, defeating the purpose of throttling:

if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}

This pattern of "cancel the old, schedule the new" is what gives us the "remember only the latest" behavior.

Leading vs Trailing Throttle

This solution implements both leading and trailing execution:

  • Leading: The first call fires immediately (since enough time has passed).
  • Trailing: If calls happen during cooldown, the last one fires after the delay.

Some throttle implementations only do leading (ignore calls during cooldown) or only trailing (always delay). This combination is the most practical for real-world event handlers because it gives instant feedback on the first interaction while still capturing the final state.

Resources

00:00