Among the RxJs operators that developers reach for most often in everyday work are the higher-order mapping operators: switchMap, mergeMap, concatMap, and exhaustMap.
Nearly every network call in a typical application is routed through one of these four, so a solid grasp of them is a prerequisite for writing just about any reactive program.
Deciding which operator fits a particular scenario—and understanding why—can feel tricky. It is easy to get tangled up in questions about how they actually behave internally and where their names come from.
These operators may appear to be separate topics, but it is best to learn them together. Picking the wrong one can introduce subtle, hard-to-trace bugs into an application.
Why mapping operators can feel confusing
The confusion has a clear source: to truly understand these operators, you first have to understand the Observable combination strategy each one relies on internally.
Rather than trying to learn switchMap in isolation, you need to first understand what Observable switching is. Instead of jumping straight into concatMap, you first need to learn about Observable concatenation, and so on.
That is exactly the path we will take here. We will work through the concat, merge, switch, and exhaust strategies in a logical order, pairing each with its corresponding mapping operator: concatMap, mergeMap, switchMap, and exhaustMap.
The concepts will be explained with marble diagrams and practical examples, including runnable code.
By the end, you will know precisely how each mapping operator works, in which situations each one is appropriate, and why they carry the names they do.
Table of Contents
This post covers the following topics:
- The RxJs Map Operator
- What is higher-order Observable Mapping
- Observable Concatenation
- The RxJs concatMap Operator
- Observable Merging
- The RxJs mergeMap Operator
- Observable Switching
- The RxJs switchMap Operator
- The Exhaust strategy
- The RxJs exhaustMap Operator
- How to choose the right mapping Operator?
- Running GitHub repo (with code samples)
- Conclusions
This post is part of our ongoing RxJs Series. Let's dive straight into the deep end with these mapping operators.
The RxJs Map Operator
Let's start at the foundation and clarify what these mapping operators do in general.
As their names suggest, they perform some sort of mapping—but what exactly is being mapped? Here is the marble diagram for the base RxJs Map operator:
How the base Map Operator works
With the map operator, an input stream that emits values 1, 2, 3 can be turned into a derived output stream emitting 10, 20, 30.
The output values come from applying a function to each input value; in this case, the function multiplies each value by 10.
The map operator, then, is all about transforming the values of the input observable. Here is a typical use case, handling an HTTP request:
In this example, we create an HTTP observable that performs a backend call and subscribe to it. That observable emits the backend's HTTP response as a JSON object.
Since the response wraps the data in a payload property, we apply the RxJs map operator. The mapping function extracts the value of that payload property from the JSON response.
Now that base mapping is clear, we can turn to higher-order mapping.
What is Higher-Order Observable Mapping?
Higher-order mapping takes a different approach: instead of mapping a plain value like 1 to another plain value like 10, we map a value into an Observable.
The result is a higher-order Observable—an Observable like any other, except that its values are themselves Observables, which can be subscribed to independently.
This might sound abstract, but it happens all the time in practice. Here is a concrete example. Suppose we have an Angular Reactive Form that emits valid form values over time through an Observable:
The Reactive Form exposes an Observable this.form.valueChanges that emits the latest form values as the user interacts with the form. That is our source Observable.
Our goal is to save at least some of these values as they are emitted, building a draft pre-save feature. That way, data is progressively persisted while the user fills out the form, preventing loss of the entire form due to an accidental reload.
Why Higher-Order Observables?
To implement the draft save feature, we need to take each form value and create a second HTTP observable that performs a backend save, and then subscribe to it.
We could attempt to handle all of this manually, but that would lead us straight into the nested subscribes anti-pattern:
As shown, this approach causes code to nest at multiple levels very quickly—the very problem RxJs is meant to solve.
Let's call this newly created
httpPost$Observable the inner Observable, since it is created within an inner, nested block of code.
Avoiding nested subscriptions
A much more convenient approach is to take the form value and map it directly into a save Observable. That creates a higher-order Observable where each emitted value represents a save request.
We then want to transparently subscribe to each of these network Observables and receive their responses in a single, flat stream—no nesting required.
That is exactly what a higher-order RxJs mapping operator provides. So why do we need four distinct operators?
The reason becomes clear when we consider what happens if the valueChanges Observable emits multiple form values in rapid succession while the save operation is still incomplete:
- should we wait for one save request to finish before starting the next?
- should multiple saves run in parallel?
- should we cancel an ongoing save when a new one begins?
- should we ignore new save attempts while one is already in progress?
Before exploring each scenario, let's revisit the nested-subscribes code above.
In that example, save operations are triggered in parallel, which is undesirable because there is no guarantee that the backend processes saves sequentially and ends up storing the most recent valid form value.
Let's see what it would take to ensure that a save request begins only after the previous one has completed.
Understanding Observable Concatenation
To implement sequential saves, we introduce the concept of Observable concatenation. In the following example, we concatenate two observables using the concat() RxJs function:
We create two Observables, series1$ and series2$, with the of creation function. We then create a third Observable, result$, as the concatenation of the two sources.
Here is the console output from this program, showing the values emitted by the result Observable:
a
b
x
y
The output is simply the values of series1$ followed by those of series2$. There's a catch, though: this works only because both Observables are completing!
The of() function creates Observables that emit the passed-in values and then immediately complete.
Observable Concatenation Marble Diagram
To see what is really happening, we need the marble diagram for Observable concatenation:
Notice the vertical bar after the value b on the first Observable? That marks the moment when the first Observable, series1$ with values a and b, completes.
Let's walk through the timeline step by step:
- the two Observables
series1$andseries2$are passed toconcat() concat()subscribes to the first Observable,series1$, but not to the second,series2$(this is the critical aspect of concatenation)source1$emits value a, which is immediately forwarded to the outputresult$Observable- note that
source2$is not yet emitting anything, because it has not been subscribed to source1$then emits b, which appears in the output- only after
source1$completes doesconcat()subscribe tosource2$ - values from
source2$then flow into the output untilsource2$completes - once
source2$completes, theresult$Observable also completes - you can pass as many Observables to
concat()as you like, not just two as in this example
The key point about Observable Concatenation
Observable concatenation is fundamentally about completion. We consume the first Observable's values, wait for it to finish, then move on to the next, and so on until all are done.
Let's see how this idea of concatenation applies to our higher-order mapping example.
Using Observable Concatenation to implement sequential saves
To guarantee that form values are saved sequentially, we need to take each form value and map it to an httpPost$ Observable.
We then need to subscribe to it, but crucially, we want the save to complete before subscribing to the next httpPost$ Observable.
To enforce sequentiality, we need to concatenate the multiple
httpPost$Observables together!
We will subscribe to each httpPost$ and handle the results one by one. What we really need is an operator that combines two things:
- a higher-order mapping step (turning the form value into an
httpPost$Observable) - a
concat()-style combination, chaining thehttpPost$Observables so that no save begins before the previous one finishes
The operator we need is aptly named RxJs concatMap Operator, which fuses higher-order mapping with Observable concatenation.
The RxJs concatMap Operator
Here is what the code looks like when we apply concatMap:
The first advantage is obvious: no more nested subscribes.
With concatMap, all form values will be sent to the backend in strict sequence, as seen in the Chrome DevTools Network tab:
Breaking down the concatMap network log diagram
Notice how each save request starts only after the previous one has completed. Here is how concatMap enforces that order:
- concatMap converts each form value into a save HTTP Observable, referred to as an inner Observable
- concatMap subscribes to that inner Observable and relays its output to the result Observable
- a second form value can arrive faster than the backend can finish saving the first one
- in that case, the new value is not immediately mapped to an HTTP request
- instead, concatMap waits for the ongoing HTTP Observable to complete before mapping the new value to a new HTTP Observable, subscribing to it, and thus starting the next save
The code shown here is a basic draft-save implementation. You can combine it with other operators to save only valid form values or to throttle saves so they don't happen too frequently.
Observable Merging
Concatenation is a solid way to order a series of HTTP saves, but there are situations where running things in parallel is preferable—no waiting for the previous inner Observable to complete.
That is where the merge strategy comes in! Unlike concat, merge does not wait for one Observable to complete before subscribing to the next.
Instead, merge subscribes to every source Observable immediately, and then forwards each emitted value to the result Observable as it arrives, regardless of timing.
Practical Merge Example
To show that merge does not depend on completion, let's merge two Observables that never complete—interval Observables:
Observables built with interval() emit 0, 1, 2, etc., every second, and they never complete.
A couple of map operators are applied to these interval Observables just to make them easy to tell apart in the console output.
Here are the first few values shown in the console:
0
0
10
100
20
200
30
300
Merging and Observable Completion
As you can see, values from the merged sources appear in the result Observable immediately as they are emitted. If one of the merged Observables completes, merge continues to relay values from the remaining Observables until they're done too.
Note that even if the source Observables eventually complete, merge behaves the same way.
The Merge Marble Diagram
Here is another merge example, rendered as a marble diagram:
The values from each merged source show up in the output as soon as they occur. The result Observable will not complete until all merged Observables have completed.
With the merge strategy understood, let's apply it to higher-order Observable mapping.
The RxJs mergeMap Operator
Combining the merge strategy with higher-order mapping produces the RxJs mergeMap Operator. Here is its marble diagram:
Here is how mergeMap works:
- each value of the source Observable is mapped to an inner Observable, just like with concatMap
- mergeMap subscribes to that inner Observable as well
- as the inner Observables emit values, those values are immediately pushed to the output Observable
- however, unlike concatMap, mergeMap does not wait for the previous inner Observable to finish before starting the next one
- as a result, multiple inner Observables can overlap in time and emit values in parallel, as highlighted in red in the diagram
Checking the mergeMap Network Log
Going back to our draft-save example, it is clear that concatMap is the correct choice here—not mergeMap—because we don't want saves running in parallel.
Let's see what happens if mergeMap is used by mistake:
If the user fills in the form quickly, the network log will now show multiple save requests running simultaneously:
The requests are parallel, which in this case is a bug: under heavy load, these requests could easily be processed out of order.
Observable Switching
Next up is another combination strategy: switching. Switching is closer to merging than to concatenation, because we do not wait for any Observable to terminate.
But unlike merging, when a new Observable begins emitting values, we unsubscribe from the previous Observable before subscribing to the new one.
Observable switching is all about ensuring that unsubscription logic for obsolete Observables is triggered, so resources can be freed!
Switch Marble Diagram
Here is the marble diagram for switching:
The diagonal lines are intentional. With the switch strategy, the higher-order Observable—the top line—needs to be shown, since it emits Observables.
The point where a diagonal line forks from the top line is when a value Observable is emitted and subscribed to by switch.
Breaking down the switch Marble Diagram
Here is what happens in this diagram:
- the higher-order Observable emits its first inner Observable (a-b-c-d), which switch subscribes to
- that inner Observable emits a and b, which appear immediately in the output
- then the second inner Observable (e-f-g) is emitted, which triggers the unsubscription from the first inner Observable (a-b-c-d)—the essence of switching
- the second inner Observable (e-f-g) then begins emitting, and those values appear in the output
- meanwhile, the first inner Observable (a-b-c-d) continues to emit c and d, but its output is not shown, because we have already unsubscribed from it
This explains the unusual diagonal layout: it visually represents when each inner Observable is subscribed to or unsubscribed from, at the points where the diagonal lines separate from the source higher-order Observable.
The RxJs switchMap Operator
Now let's apply the switch strategy to higher-order mapping. Suppose we have a plain input stream emitting the values 1, 3, and 5.
We map each value to an Observable, as we did with concatMap and mergeMap, yielding a higher-order Observable.
If we switch between the emitted inner Observables—rather than concatenating or merging them—we get the switchMap Operator:
Breaking down the switchMap Marble Diagram
Here is how this operator behaves:
- the source Observable emits 1, 3, and 5
- each value is mapped to an Observable via a function
- switchMap subscribes to each mapped inner Observable
- values emitted by the inner Observables appear in the output right away
- however, if a new value like 5 arrives before the previous Observable has completed, the earlier inner Observable (30-30-30) is unsubscribed from, and its subsequent values no longer appear in the output
- notice the 30-30-30 inner Observable in red: its final 30 value is missing because that Observable was unsubscribed from
Switching, then, is all about ensuring that unsubscription logic runs for Observables we no longer care about. Let's see switchMap in action!
Search TypeAhead - switchMap Operator Example
A classic use case for switchMap is a search typeahead. First, we define our source Observable, whose values will trigger search requests.
That source Observable emits the search text the user types into an input:
As the user types "Hello World" as a search query, the values emitted by searchText$ look like this:
H
H
He
Hel
Hell
Hello
Hello
Hello W
Hello W
Hello Wo
Hello Wor
Hello Worl
Hello World
Debouncing and removing duplicates from a Typeahead
Notice the duplicate values, which arise from spaces between words or the Shift key used for capitals like H and W.
To avoid sending all of these values to the backend as separate requests, we can use the debounceTime operator to wait for the user to pause typing:
With that operator, if the user types at a normal speed, searchText$ will output only one value:
Hello World
This is a big improvement: a value is emitted only if it remains stable for at least 400ms.
But if the user types slowly—pausing for more than 400ms between keys—the search stream might look like this:
He
Hell
Hello World
Similarly, a user might type a value, hit backspace, then type it again, creating duplicate searches. The distinctUntilChanged operator can prevent those duplicates from being sent.
Cancelling obsolete searches in a Typeahead
Beyond that, we need a way to cancel previous searches when a new one starts.
Our goal is to transform each search string into a backend search request, subscribe to it, and apply the switch strategy between consecutive searches—cancelling the previous request when a new one is triggered.
That is precisely what switchMap does! Here is the final typeahead implementation using it:
switchMap Demo with a Typeahead
Here's switchMap in action. If the user types, hesitates, and then types something else, a typical network log looks like this:
Several previous searches are cancelled while still in flight, which is great for freeing up server resources for other work.
The Exhaust Strategy
switchMap is perfect for the typeahead case, but there are scenarios where we want to ignore new source values until the current one has been fully processed.
For example, suppose we trigger a backend save in response to a click on a save button. We might first try concatMap to ensure saves run in sequence:
That guarantees order, but what if the user clicks the save button multiple times? Here is what shows up in the network log:
Each click causes a save: 20 clicks mean 20 saves. In this case, we want more than just sequential saves.
We also want to ignore a click—but only if a save is already in progress. The exhaust strategy enables exactly that.
Exhaust Marble Diagram
Here is how exhaust works, illustrated with a marble diagram:
Just like before, the top line represents a higher-order Observable whose values are Observables, forking from the top line. Here's what happens:
- just like switch, exhaust subscribes to the first inner Observable (a-b-c)
- values a, b, and c appear in the output as usual
- a second inner Observable (d-e-f) is emitted while the first (a-b-c) is still active
- this second Observable is discarded by exhaust—it is never subscribed to (this is the core of exhaust)
- only after the first Observable (a-b-c) completes will exhaust subscribe to new ones
- when a third Observable (g-h-i) is emitted, the first has already finished, so this one is not discarded—it is subscribed to
- the values g-h-i from the third Observable show up in the result, while d-e-f do not appear at all
Now we can apply the exhaust strategy in the context of higher-order mapping, just as we did with concat, merge, and switch.
The RxJs exhaustMap Operator
Here is the marble diagram for exhaustMap. Remember, unlike the previous diagram's top line, the source Observable 1-3-5 here emits plain values, not Observables.
These values could, for instance, represent mouse clicks:
Here is what happens in the exhaustMap diagram:
- the value 1 is emitted, creating an inner Observable 10-10-10
- that Observable emits all its 10s and completes before the source emits 3, so all of them appear in the output
- a new value 3 is emitted, giving rise to a 30-30-30 inner Observable
- while 30-30-30 is still running, the source emits value 5
- the value 5 is discarded by exhaust—no 50-50-50 Observable is created, so no 50 values show up in the output
A Practical Example for exhaustMap
Let's apply exhaustMap to our save-button scenario:
If we click save five times in a row, the network log looks like this:
As expected, clicks made while a save request was ongoing were ignored.
Note that if you keep clicking, say, 20 times, eventually the ongoing save will finish and a second save will begin.
How to choose the right mapping Operator?
concatMap, mergeMap, switchMap, and exhaustMap behave similarly in that they are all higher-order mapping operators.
Yet they differ in subtle and important ways, so there isn't a single operator that can be recommended as a universal default.
Instead, the right choice depends on the use case:
-
for sequential processing that waits for completion, concatMap is the way to go
-
for parallel execution, choose mergeMap
-
if cancellation logic is needed, switchMap is your option
-
to ignore new Observables while one is still active, exhaustMap does exactly that
Running GitHub repo (with code samples)
If you want to try the examples yourself, here is a playground repository with all the runnable code from this post.
The repository includes a small HTTP backend for testing the mapping operators in a more realistic environment, along with live examples like draft form pre-saving, a typeahead, subjects, and components written in a Reactive style:
Conclusions
The RxJs higher-order mapping operators are indispensable for common reactive programming tasks, such as making network calls.
To truly understand these operators and their names, it is essential to first focus on the underlying Observable combination strategies: concat, merge, switch, and exhaust.
It's also crucial to recognize that a higher-order mapping operation is taking place, where values are transformed into separate Observables, which are then subscribed to implicitly by the mapping operator itself.
Selecting the right operator is really about selecting the right inner-Observable combination strategy. Choosing incorrectly often doesn't break the program immediately, but it can lead to subtle issues that are difficult to debug later.
We hope you enjoyed this post! To go much deeper on RxJs, we recommend the RxJs In Practice Course, which covers many useful patterns and operators in greater detail.
Questions or comments? Leave them below, and we'll get back to you.
To stay updated on upcoming RxJs and Angular posts, subscribe to our newsletter:
If you're just starting with Angular, have a look at the Angular for Beginners Course:
