Design Debounced Search
A timer-backed async wrapper that collapses rapid inputs into one search, resolves pending promises, caches exact terms, and guards stale results.
- Pattern
- Timer -> pending promises -> stale guard
- Time
- O(1) per input
- Space
- O(pending + cache)
Debounced search is where callback mechanics, timers, promises, and UI state all meet.
The recipe
Say before you type: "Clear the old timer. Save the latest term. Resolve the whole pending batch when the timer fires."
Each search(term) returns a promise. During a burst, collect the promise resolvers:
private pending: Array<{ resolve: (value: string[]) => void; reject: (error: unknown) => void }> = [];
When the timer fires, copy the batch, clear pending, run the searcher once with the latest term, then resolve every waiter with the result.
Normalise the searcher's return value — Promise.resolve(this.searcher(term)).then(…) or await — because the declared type is Promise<string[]> | string[] and a synchronous searcher has no .then. Calling .then() on the raw return value is the trap, and it is a nasty one: the TypeError is thrown inside your timer callback, where nothing can catch it, so it surfaces as a dead run rather than a failed test.
Cache
An exact-term cache is just:
private cache = new Map<string, string[]>();
If the term is cached, return Promise.resolve(cached) and do not wait for the debounce delay.
Stale guard
A slow old request can finish after a newer one. Keep a sequence number. When a search starts, capture the current sequence. Only update latest() if the captured sequence is still current when it resolves.