Choosing the Right Flattening Operator in RxJS: switchMap, mergeMap, concatMap, and exhaustMap
A technical guide for choosing between switchMap, mergeMap, concatMap, and exhaustMap in RxJS to manage asynchronous concurrency and prevent race conditions.
07 Jan 2026, 20:20 UTC

The Concurrency Dilemma in Asynchronous Streams
When dealing with nested observables—where a source stream triggers another asynchronous operation (like an API call)—you face a concurrency problem: what happens when the source emits a new value before the previous asynchronous operation has finished? Choosing the wrong flattening operator can lead to race conditions, duplicate data entries, or unresponsive user interfaces.
Comparing Flattening Strategies
Flattening operators take an observable that emits other observables (a Higher-Order Observable) and "flatten" them into a single stream of values. The difference lies in how they handle the "inner" observable's lifecycle relative to the "outer" source.
| Operator | Concurrency | Ordering | Behavior on New Emission |
|---|---|---|---|
switchMap |
Single (Latest) | Latest only | Cancels current inner observable immediately. |
mergeMap |
Parallel | Unpredictable | Starts a new inner observable concurrently. |
concatMap |
Sequential | Preserved | Queues the new inner observable until current finishes. |
exhaustMap |
Single (First) | First only | Ignores new emissions until current finishes. |
Engineering Trade-offs and Risks
When to use switchMap
Use switchMap for read-only operations where only the most recent request matters. A classic example is a search-as-you-type input. If the user types "A", then "AB", the request for "A" is no longer relevant and should be cancelled to save bandwidth and prevent an older request from overwriting a newer one.
Risk: Never use switchMap for POST or PUT requests that modify server state. If the observable is cancelled, the client may stop listening, but the server might still process the write, leading to a mismatch between UI state and database state.
When to use mergeMap
Use mergeMap when requests are independent and the order of completion does not matter. For example, deleting multiple items from a list where each delete request is a separate network call.
Risk: mergeMap provides no backpressure. If the source emits 100 values per second and the inner observable takes 1 second to complete, you will have 100 concurrent network requests, potentially triggering rate limits or crashing the browser tab.
When to use concatMap
Use concatMap when the sequence of operations is critical. If you are uploading a series of files that must be processed in a specific order, concatMap ensures that the second upload does not start until the first is fully completed.
Risk: It creates a bottleneck. If a single request in the queue hangs or takes an unusually long time, all subsequent emissions are blocked, creating a perceived "freeze" in the application.
When to use exhaustMap
Use exhaustMap to prevent duplicate submissions. This is ideal for "Submit" buttons. Once the user clicks submit, any further clicks are ignored until the server responds, preventing the creation of duplicate records.
Implementation and Validation Example
To verify these behaviors, you can simulate a delayed API call using of().pipe(delay()). The following example demonstrates how to implement a search feature using switchMap to ensure only the latest result is rendered.
import { fromEvent, of } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { switchMap, debounceTime, distinctUntilChanged, catchError } from 'rxjs/operators';
// Run this in a browser environment with an HTML input element #search-input
const searchInput = document.getElementById('search-input');
fromEvent(searchInput, 'input').pipe(
// 1. Wait for user to stop typing for 300ms
debounceTime(300),
// 2. Only emit if the value actually changed
distinctUntilChanged(),
// 3. Cancel previous request and switch to the new one
switchMap(event => {
const term = event.target.value;
return ajax(`/api/search?q=${term}`).pipe(
catchError(err => {
console.error('Search failed', err);
return of([]); // Return empty array on error to keep outer stream alive
})
);
})
).subscribe(results => {
console.log('Rendering results:', results);
});
How to Verify the Result
To validate that switchMap is working as intended:
- Open the browser's Network Tab.
- Type rapidly into the search input.
- Observe that previous pending requests are marked as
(canceled)in the network log as soon as a new character is typed (after the debounce period). - Verify that the final rendered result matches the final string entered, regardless of which network request finished first.
If you replace switchMap with mergeMap in the example above, you will notice that requests are not cancelled, and if an earlier request takes longer than a later one, the UI will "flicker" as the older data arrives last and overwrites the newer data.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.