Using RxJS switchMap to Cancel Out‑of‑Order Search Requests
Learn how switchMap automatically cancels previous HTTP requests in a search‑as‑you‑type flow, ensuring the UI shows only the latest result.
01 Jul 2025, 21:55 UTC

Problem: Stale search results overwriting newer ones
When a user types quickly into a search box, each keystroke can trigger an HTTP request. If a earlier request resolves after a later one, the UI may display outdated data, causing a race condition.
Desired outcome
- Only the result from the most recent keystroke is shown.
- Previous requests are cancelled as soon as a new value arrives.
- The UI updates without flicker from stale data.
Prerequisites
- Basic knowledge of Observables and RxJS operators.
- RxJS version 7.x or later (the behavior of
switchMapis stable across recent versions). - An HTTP client that returns an Observable (e.g., Angular’s
HttpClientorfetchwrapped withfrom). - Permission to make network calls to your search endpoint (typically no special privileges beyond standard CORS).
Procedure
-
Create an observable from the input element that emits the current value on each
inputevent.import { fromEvent } from 'rxjs'; import { map, debounceTime, switchMap } from 'rxjs/operators'; const input = document.getElementById('search-box'); const keyups$ = fromEvent(input, 'input').pipe( map(e => e.target.value), ); -
Apply
debounceTimeto limit how often a new request is started (e.g., 300 ms). This reduces unnecessary calls while still feeling responsive.const debounced$ = keyups$.pipe(debounceTime(300)); -
Use
switchMapto turn each debounced value into an HTTP request observable. When a new value arrives,switchMapunsubscribes from the previous inner observable, effectively cancelling the pending request.const results$ = debounced$.pipe( switchMap(term => { if (!term) return []; // optional: skip empty queries return http.get(`/api/search?q=${encodeURIComponent(term)}`); }) ); -
Subscribe to the resulting observable and update the UI with the latest data.
results$.subscribe({ next: data => { renderResults(data); // your UI update function }, error: err => console.error('Search failed', err) });
Expected checks
- Open browser developer tools → Network tab. While typing rapidly, observe that previous requests show a status of
canceledas soon as a new character is typed. - Verify that the final rendered result matches the value of the last completed request (i.e., the query corresponding to the final pause in typing).
- Introduce artificial latency (e.g., using Chrome’s throttling to “Slow 3G”) and confirm that out‑of‑order responses do not overwrite the UI.
Limitations and practical verification
The cancellation performed by switchMap works only if the inner observable supports aborting (e.g., an HttpClient request that respects XMLHttpRequest.abort() or the Fetch API’s AbortController). If you wrap a raw Promise that does not expose a cancellation mechanism, the request will continue on the server even though the subscription is dropped.
To verify that cancellation is effective, you can:
- Add a server‑side log that records when a request starts and ends.
- Compare the log timestamps with the browser’s Network tab; a request marked
canceledshould have no corresponding “finished” log after the cancel point. - If you see completed logs for requests that were marked canceled in the browser, your inner observable does not support true cancellation.
Recovery options
If you need to guarantee that a request reaches the server (e.g., a “save” operation), avoid switchMap because it may discard in‑flight requests. Instead consider:
mergeMap– lets multiple requests run in parallel; handle ordering yourself.exhaustMap– ignores new values while a request is active, ensuring the current request finishes before the next can start.
Choose the operator that matches the semantics of your operation: switchMap for cancellable look‑ups, exhaustMap for fire‑and‑forget actions where you want to skip rapid repeats, and mergeMap when you truly need concurrent requests.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.