An SDET interview asked me to implement debounce and throttle and then prove they work. How do I write them, and how do I test time-based code without slow, flaky waits?
Asked by The SDET Playbook
Asked Sep 28, 2026Viewed 0 times
An SDET interview asked me to implement debounce and throttle and then prove they work. How do I write them, and how do I test time-based code without slow, flaky waits?
Asked by The SDET Playbook
Sign in to answer and to vote.
Debounce waits until calls stop for a given delay, then runs once with the last arguments (a search box waiting for the user to stop typing). Throttle runs at most once per interval, however often it's called (a scroll handler).
export function debounce<A extends unknown[]>(fn: (...args: A) => void, waitMs: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: A): void => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), waitMs);
};
}
export function throttle<A extends unknown[]>(fn: (...args: A) => void, intervalMs: number) {
let last = -Infinity;
return (...args: A): void => {
const now = Date.now();
if (now - last >= intervalMs) {
last = now;
fn(...args);
}
};
}
This throttle runs on the leading edge and drops calls inside the interval. Say so, and offer a trailing call as an option if the last event must not be lost.
Never test these with real sleeps: the tests get slow and flaky. Fake the clock instead. Vitest's fake timers also control Date.now():
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { debounce, throttle } from './timing';
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
describe('debounce', () => {
it('runs once, with the last arguments, after calls stop', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced('a'); debounced('b');
vi.advanceTimersByTime(299);
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith('b');
});
});
describe('throttle', () => {
it('runs at most once per interval', () => {
const fn = vi.fn();
const throttled = throttle(fn, 100);
throttled(); throttled();
expect(fn).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(100);
throttled();
expect(fn).toHaveBeenCalledTimes(2);
});
});
The same idea works in end-to-end tests. Playwright's page.clock lets you install a fake clock in the browser and move it forward, so you can check a debounced search without waiting in real time.
Sources: MDN: debounce, MDN: throttle, Vitest: fake timers, Playwright clock