Narrate throttle with trailing calls
Build throttle with trailing calls. The interviewer expects a small, reusable utility with clear behavior under repeated calls and invalid inputs.
Answer Strategy
For throttle with trailing calls, start by stating the public contract before writing code: argument shape, return shape, mutation rules, error behavior, and whether work is synchronous, timed, cached, or cancellable.
A senior solution uses boring names for hidden state. If the function stores a timer, cache entry, listener, or in-flight promise, say who owns that state and how it is cleaned up.
After the baseline passes, harden the edge cases: empty input, repeated calls, invalid values, thrown callbacks, stable ordering, and memory lifetime. The reference below is written to be narrated line by line.
Reference Implementation: Throttle With Trailing Call
Throttle should preserve the first call in a window and optionally replay the latest suppressed call at the end.
type Throttled<TArgs extends unknown[]> = {
(...args: TArgs): void;
cancel(): void;
flush(): void;
};
function throttle<TArgs extends unknown[]>(
fn: (...args: TArgs) => void,
waitMs: number
): Throttled<TArgs> {
let lastRunAt = Number.NEGATIVE_INFINITY;
let trailingArgs: TArgs | null = null;
let trailingTimer: ReturnType<typeof setTimeout> | null = null;
function clearTrailing() {
if (trailingTimer !== null) clearTimeout(trailingTimer);
trailingTimer = null;
}
function run(args: TArgs) {
lastRunAt = Date.now();
fn(...args);
}
function schedule() {
clearTrailing();
const remaining = Math.max(0, waitMs - (Date.now() - lastRunAt));
trailingTimer = setTimeout(() => {
trailingTimer = null;
if (!trailingArgs) return;
const args = trailingArgs;
trailingArgs = null;
run(args);
}, remaining);
}
function throttled(...args: TArgs) {
if (Date.now() - lastRunAt >= waitMs) {
trailingArgs = null;
clearTrailing();
run(args);
return;
}
trailingArgs = args;
schedule();
}
throttled.cancel = () => {
trailingArgs = null;
clearTrailing();
};
throttled.flush = () => {
if (!trailingArgs) return;
const args = trailingArgs;
trailingArgs = null;
clearTrailing();
run(args);
};
return throttled;
}Runnable Playground
Edit the implementation and run the tests directly in the browser. For system design questions, the playground focuses on the core state/data logic that the UI would call.
type Throttled<TArgs extends unknown[]> = {
(...args: TArgs): void;
cancel(): void;
flush(): void;
};
function throttle<TArgs extends unknown[]>(
fn: (...args: TArgs) => void,
waitMs: number
): Throttled<TArgs> {
let lastRunAt = Number.NEGATIVE_INFINITY;
let trailingArgs: TArgs | null = null;
let trailingTimer: ReturnType<typeof setTimeout> | null = null;
function clearTrailing() {
if (trailingTimer !== null) clearTimeout(trailingTimer);
trailingTimer = null;
}
function run(args: TArgs) {
lastRunAt = Date.now();
fn(...args);
}
function schedule() {
clearTrailing();
const remaining = Math.max(0, waitMs - (Date.now() - lastRunAt));
trailingTimer = setTimeout(() => {
trailingTimer = null;
if (!trailingArgs) return;
const args = trailingArgs;
trailingArgs = null;
run(args);
}, remaining);
}
function throttled(...args: TArgs) {
if (Date.now() - lastRunAt >= waitMs) {
trailingArgs = null;
clearTrailing();
run(args);
return;
}
trailingArgs = args;
schedule();
}
throttled.cancel = () => {
trailingArgs = null;
clearTrailing();
};
throttled.flush = () => {
if (!trailingArgs) return;
const args = trailingArgs;
trailingArgs = null;
clearTrailing();
run(args);
};
return throttled;
}Testing Strategy
Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.
vi.useFakeTimers();
vi.setSystemTime(0);
test('throttle runs immediately and keeps the latest trailing call', () => {
const calls: string[] = [];
const save = throttle((value: string) => calls.push(value), 100);
save('a');
save('b');
save('c');
expect(calls).toEqual(['a']);
vi.advanceTimersByTime(100);
expect(calls).toEqual(['a', 'c']);
save('d');
save.cancel();
vi.advanceTimersByTime(100);
expect(calls).toEqual(['a', 'c']);
});Interviewer Signal
Tests whether you can turn a familiar utility into a precise contract instead of coding only the happy path.
Constraints
- Define the function signature before coding.
- Do not rely on global mutable state unless it is part of the returned closure.
- Explain time and space cost for the common path.
Model Answer Shape
- Write the smallest public contract first.
- Cover empty input, repeated calls, thrown errors, and cleanup behavior.
- Keep implementation readable enough to narrate under interview pressure.
Tradeoffs
- A compact implementation is attractive, but explicit state names are easier to debug live.
- Supporting every possible input can distract from the core contract; state the scope before coding.
Edge Cases
- No arguments or undefined callbacks.
- Synchronous throw inside the wrapped function.
- Repeated calls before the previous result settles.
Testing And Proof
- Happy path with representative inputs.
- Boundary input and repeated invocation.
- Cleanup or cancellation if timers or promises are involved.
Follow-Ups
- How would you expose cancellation?
- How would the API change for React usage?