Compile Ready
Module 14 · Machine Coding

Build: Debounced Search

Intermediate12m read35m practice47m total
Machine CodingDebounceAsyncUI

Introduction

A debounced search box waits until the user pauses typing before it calls the expensive search operation. A production-quality version also guards against stale responses, cancels work when possible, renders loading and empty states, and exposes cleanup for tests and unmounting.

This is one of the most common frontend machine-coding rounds because it combines timers, closures, async ordering, UI state, and edge-case thinking.

Why This Matters

Interviewers use debounced search to separate candidates who only know the debounce utility from candidates who can ship resilient UI. The real bug is often not too many requests; it is an older slow response overwriting a newer fast response. Naming that race condition and guarding it is a Staff-level signal.

Theory

Design goals

  1. Keep typing responsive by delaying the search call until the user pauses.
  2. Avoid invalid requests for very short queries.
  3. Make async responses race-safe with a monotonically increasing request id.
  4. Abort the previous request when the provided search function supports AbortSignal.
  5. Render explicit loading, empty, error, and result states.
  6. Return a destroy method so event listeners and timers do not leak.

Architecture

The small debounce utility owns timer state and exposes cancel and flush. The feature wrapper owns UI state: current request id, current abort controller, rendering, and event listener cleanup. Every time a search starts, it increments latestRequestId; when the promise resolves, it renders only if the id still matches. That single check prevents stale response bugs even when aborting is unavailable.

Interview trade-offs

Use debounce when the user is still changing the query and the latest value is the only value that matters. Use throttle when you want periodic updates while a stream continues, such as scroll position. For search, trailing debounce is usually the default; adding a leading call can make the UI feel faster but complicates duplicate-request handling.

Visual Diagrams

Debounce plus stale-response guard
type r
type re
type rea
   |
   v
reset one timer until typing pauses
   |
   v
request id 7 starts
   |
   +--> older id 6 resolves late -> ignored
   |
   +--> id 7 resolves -> render results

The timer reduces request volume; the request id protects correctness.

Code Examples

Complete debounced search widget

The implementation is DOM-based for the actual build, but the logic is separated enough to test the debounce and stale-response guard independently.

Loading…

Playground

Loading editor…
Console

Press Run to execute the code and see output here.

Output Prediction

Predict the output #1

javascript
1function debounce(fn, delay) {
2 var timerId = null;
3 var lastArgs = null;
4
5 function debounced() {
6 lastArgs = arguments;
7 clearTimeout(timerId);
8 timerId = setTimeout(function () {
9 fn.apply(null, lastArgs);
10 }, delay);
11 }
12
13 debounced.flush = function () {
14 clearTimeout(timerId);
15 fn.apply(null, lastArgs);
16 };
17
18 return debounced;
19}
20
21var calls = [];
22var search = debounce(function (query) {
23 calls.push(query);
24 console.log('search:' + query);
25}, 20);
26
27search('r');
28search('re');
29search('react');
30
31setTimeout(function () {
32 search('react j');
33 search.flush();
34 console.log('calls=' + calls.join(','));
35}, 30);

Coding Exercises

Implement a race-safe debounced search widget

Medium

Build createDebouncedSearch(options) for a search input.

Requirements:

  • Accept { input, results, search, minLength, delay }.
  • Debounce input events before calling search(query, signal).
  • Ignore stale responses that resolve after a newer request.
  • Abort the previous request when AbortController exists.
  • Render loading, empty, error, and success states.
  • Return { destroy, flush } for cleanup and tests.

Constraints:

  • Do not use libraries.
  • Do not rely on global mutable state.
  • The implementation must be safe if promises resolve out of order.

Interview Questions

1How do you prevent stale search results from rendering?

Assign each request a monotonically increasing id. Store the newest id. When a promise resolves or rejects, compare its id with the latest id and render only if they match. AbortController is useful but not sufficient because some async functions cannot be cancelled or may still settle after aborting.

Asked at:GoogleMicrosoftUber

Follow-ups

  • How would you test the stale response case?
  • When would you use throttle instead of debounce?
2Should search debounce on the leading edge or trailing edge?

Trailing edge is the common default because the latest query is what matters. Leading edge can make the first response feel instant, but it often needs extra logic to avoid duplicate calls and stale UI. Many teams combine immediate cached suggestions with trailing remote search.

Quiz

1. What bug does the request id guard fix?

Summary

  • Debounce controls request volume; it does not solve async ordering by itself.
  • Use request ids to ignore stale responses and `AbortController` to cancel work when possible.
  • Expose cleanup so timers, listeners, and requests do not leak.
  • Render loading, empty, error, and success states explicitly.

Cheat Sheet

Debounced search checklist

  • debounce(fn, delay) stores timer, last args, cancel, and flush.
  • Validate short queries before searching.
  • Increment latestRequestId per request.
  • Render only when requestId === latestRequestId.
  • Abort previous request when possible.
  • Return cleanup for unmounting and tests.