In RxJS, why should a typeahead search use switchMap rather than mergeMap to call the search API for each query?
answer
- responses can return out of order
- latest query wins
- unsubscribe cancels the request
- stale results overwrite fresh ones
basics
~20 sSearch responses can return out of order. mergeMap forwards every response, so a slow reply for an old query can overwrite the newest results. switchMap unsubscribes the previous request when a new query arrives, so only the latest results arrive.
solid answer
~40 sA typeahead fires a request per query: `a`, `an`, `ang`. With `mergeMap`, all requests stay subscribed and every response is emitted as it arrives; if the reply for `an` is slower than the reply for `ang`, the list ends up showing results for `an` while the box says `ang`. `switchMap` keeps only one inner subscription: each new query unsubscribes the previous request before starting the next, so late replies for old queries are never emitted. With Angular's `HttpClient`, that unsubscription also aborts the in-flight request, saving bandwidth. In practice the pipeline is `debounceTime` to wait for a pause, `distinctUntilChanged` to skip repeated queries, then `switchMap` to the search call.
code
ts · 11 linesimport { Observable, of, debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
interface Hit { id: string; title: string; }
declare function search(q: string): Observable<Hit[]>;
declare const query$: Observable<string>;
const results$ = query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => (q.trim() ? search(q) : of([])))
);go deeper
Recall that switchMap cancels the previous search when a new query arrives, so only the latest results are shown.
Explain the out-of-order response race under mergeMap and why debounceTime reduces requests but does not fix ordering.
Build the full pipeline, including empty queries and per-request error handling so one failure does not kill the search box.
Weigh client-side cancellation against server load and caching, and decide how search behaviour should be standardised across products.
## The problem: a source of requests Every keystroke in a search box produces a query string. Mapping each query to an HTTP call creates an **inner observable per query**, and those requests overlap: the user types faster than the server answers. The flattening operator decides what happens to the older, still-running requests. Network latency is not ordered. A request sent later can complete earlier — a cache hit, a shorter result, a different server. That is the root of the bug. ## What mergeMap does `mergeMap` keeps **every** inner subscription alive and forwards values as they arrive: 1. The user types `an`; request A starts. 2. The user types `ang`; request B starts, while A is still running. 3. B answers first; results for `ang` are shown. 4. A answers late; results for `an` are shown — **overwriting** the correct list. The box says `ang`, the list shows `an`. This is the classic **stale response** race, and it appears only under real network conditions, which is why it slips through local testing. ## What switchMap does `switchMap` keeps **one** inner subscription. In RxJS 7.8, each new source value calls `unsubscribe()` on the previous inner before subscribing to the new one: - step 2 above unsubscribes request A before starting B; - A's late answer has no subscriber, so it is never emitted; - the list always matches the latest query. There is a resource benefit too. With Angular's `HttpClient`, the request observable's teardown aborts the underlying XHR or fetch call, so a cancelled query stops using the connection. ## The usual pipeline ```ts query$.pipe( debounceTime(300), distinctUntilChanged(), switchMap(q => search(q)) ) ``` | step | job | |---|---| | `debounceTime(300)` | wait for a pause, so not every keystroke becomes a request | | `distinctUntilChanged()` | skip a query equal to the previous one | | `switchMap(search)` | cancel the stale request; show only the latest results | `debounceTime` reduces the **number** of requests; only `switchMap` fixes the **ordering** problem. A debounce alone still allows two requests to overlap when the user pauses twice in quick succession. ## Edge cases interviewers probe - **An empty query**: return `of([])` from the project function instead of calling the API; `switchMap` still cancels a pending request for the previous query. - **A failed request**: an error from the inner observable errors the whole stream, and the search box stops responding. Each inner request must handle its own error to keep the typeahead alive. - **Server-side work**: cancelling on the client does not stop the server from finishing a request it already received; for a read-only search that is harmless. - **Why not `concatMap`**: it would run every query in order, so the user waits for results of queries they no longer care about. - **Why not `exhaustMap`**: it would ignore the newer queries while an old one runs, showing results for an outdated query. ## Explaining it in an interview A strong answer walks through four steps: 1. Name the race: requests overlap and responses can return out of order. 2. Show the symptom under `mergeMap`: the list shows results for an older query than the one in the box. 3. Explain the mechanism of `switchMap`: it unsubscribes the previous inner observable before subscribing to the next, so a late response has nowhere to go. 4. Add the practical extras: debounce to cut request volume, skip repeated queries, and handle errors inside the inner request. Mentioning that cancellation is a client-side guarantee — it stops the response from being used, not necessarily the server from doing the work — shows depth. ## Summary | operator | outcome for a typeahead | |---|---| | `mergeMap` | stale responses can overwrite fresh results | | `concatMap` | correct order, but growing lag behind the typing | | `exhaustMap` | newer queries dropped while one is in flight | | `switchMap` | latest query only; stale requests cancelled |
- Does debounceTime alone prevent stale search results?No. It reduces how many requests start, but two requests can still overlap when the user pauses, types and pauses again before the first reply. Only `switchMap` guarantees that an older response is never emitted after a newer query.
- What does switchMap's cancellation do to an HttpClient request in an Angular app?Unsubscribing runs the request observable's teardown, which aborts the underlying XHR or fetch call if it has not finished. The browser stops waiting for it, although the server may still complete the work it already received.
saying these in an interview costs you the question
- HTTP responses always arrive in the order the requests were sent.
- debounceTime alone fixes out-of-order search results.
- switchMap waits for the running search to finish before starting the next.
- concatMap is best for a typeahead because it keeps order.
- Cancelling a request on the client guarantees the server never processed it.