Throttle Event Handler
Prompt
You have a makeRequest function that fires an API call and a basic throttle stub. Complete the throttle function so that:
- The first call fires the callback immediately
- Subsequent calls within the delay window are "remembered," and only the latest one fires after the delay expires
- The delay timer resets based on when the callback was last actually executed, not when the last call came in
- The throttle should work as a generic higher-order function that accepts any callback and delay
const throttle = (callback, delay) => (payload) =>
callback(payload);Playground
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.
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.
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. A basic throttle simply ignores all calls during the cooldown. This version is smarter. It holds onto the most recent call and ensures it fires after the delay, so 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 store the 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 (since Date.now() - 0 will always be larger than any delay). timeoutId stores the ID of any scheduled future call, so we can cancel it if a newer event comes in.
When the returned function is called, we first figure out how much time has passed since the last execution:
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;
}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.
Here's a trace through a scenario with a 5-second delay to make it concrete:
| Time | Event | What Happens |
|---|---|---|
| 0s | throttled("click-1") | timeSinceLastCall is huge (first call). Fires immediately. lastCallTime = 0s. |
| 0s | throttled("click-2") | Only 0s passed. Schedules "click-2" for 5s later. |
| 0s | throttled("click-3") | Clears the "click-2" timeout. Schedules "click-3" for 5s later. |
| 5s | (timeout fires) | "click-3" fires. lastCallTime = 5s. |
| 6s | throttled("click-4") | 1s since last call. Schedules "click-4" for 4s later. |
| 7s | throttled("click-5") | 2s since last call. Clears "click-4" timeout. Schedules "click-5" for 3s later. |
| 8s | throttled("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.
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.
Leading vs Trailing
This solution implements both leading and trailing execution. The first call fires immediately (leading), and if calls happen during cooldown, the last one fires after the delay (trailing). 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.