← Back to question bank
JS FunctionMidMedium#2006 · 35m

Harden 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;
}
TypeScript · runnable

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.

Contract
Test the public signature and return value first, including whether the utility mutates input or stores closure state.
Edges
Add empty input, repeated calls, duplicate values, thrown callbacks, and cleanup or cancellation cases.
Timing
Use fake timers for timer utilities and controlled promises for async utilities so behavior is deterministic.
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?