RxJS and the Difference Between mergeMap, switchMap, concatMap and exhaustMap With a Real-World Example

AlexAegis
The Startup
Published in
5 min readSep 6, 2020

These four operators can often confuse new developers as the difference between them is minute, but very important!

It all boils down on how the inner observable reacts when a new value comes in from the outer observable.

Preparations

What does all this mean? Let’s create a simple example. The difference only comes up if we let time pass so lets craft an example where we want to make a pipeline which receives timestamps, immediately emits that the timestamp is older than the current time, then it should emit again, once it expired.

First, set up the core logic of this in a separate function. We know that the only thing we are working with is a timestamp and that we want to craft an observable that only emits boolean so lets do that.

If the timestamp is already expired, no timer is needed so instead of over-complicating things just return an observable that will emit only a single true .

But if it’s not, lets return that it’s not, and then emit that it is, once it does. We essentially having two observables here, one for the immediate emit, that we made with of and another one that we want to emit after a set amount of time. For this we can use the timer and specify how much it needs to wait. The result of the timer is not important so lets just map the result of that to a true, to signal that it is an expired timestamp.

Then just combine the two with merge which will create an observable from it’s arguments that will emit the same thing that it’s argument emit.

Now let’s setup some observables using this function and the different higher order mapper functions that we aim to investigate!

A source Subject is there so new values can be supplied to these pipelines.

Then let’s set up our subscriptions with some extra logging for the time. Here forkJoin creates us an observable that will only emit once all inner observables complete. Start an interval that emits every second (except the first, that’s why startWith is there), and since by default interval emits indefinitely, lets make it complete when others also complete.

Now that all our subscriptions are active, lets feed some values into this pipeline so we can actually see things logged aside from the time ticking.

A little timeout ensures that logs wont mix up, the first emit happens after a 100 ms, sending a timestamp that will expire in 3 seconds, then a second later we send another that will expire an additional 4 seconds later.

Playground

Let’s see it in action!

Open up the console!

The results

Immediately all four emits because all three got that 3 second timestamp, and the inner observable sends immediately that its expired or not.

time 0
switchMap false
mergeMap false
exhaustMap false
concatMap false

After a second, a new value is added to the subject that will expire at the 5th tick. But only switch and merge emitted. It’s because exhaust doesn’t care about incoming values until the inner observable is finished! And that timer in there is till ticking for another 2 seconds. Concat does care, but it will get back to it once the first inner observable finished.

time 1
switchMap false
mergeMap false

At the fourth tick, the first timestamp expires, which means the inner observables emit. But only exhaust and merge emitted! It’s because switchMap completely thrown away the inner observable as soon as the new value came in. Exhaust did not care about the new value, and merge kept both. And concat both emitted the end of the first inner observable, and the start of the second one, because it waited out the end of the first to start with it, concatenating both.

time 3
mergeMap true
exhaustMap true
concatMap true
concatMap false

And finally at the sixth tick, the new value expired, switch and merge emitted because only they received the new value, exhaust did not emit the expiration of the second value because it was busy with the previous one. Since the second tick happens at a fixed time and not after a delay, the concat emit is here too. While the first two had a 4 second difference between their last two emits, because of this concat only had 2. If it were a fixed amount of delay, like instead of using a Date for the timer, using a number in milliseconds, the final concat emit would be at tick 7.

time 5
switchMap true
mergeMap true
concatMap true

Conclusion

So that’s the long and short of it. switchMap doesn’t care about the inner observable, exhaustMap doesn’t care about the outer observable, mergeMap cares about both immediately and concatMap cares about both but only one at a time, queueing them up.

If we need a pipeline which emits if the current value is the expired one at any given time, while it can be replaced at any time, the correct operator is switchMap, so values that are already replaced will not emit that they expired.

The example code can be found here, you can even just clone that repository, run yarn (npm i -g yarn if you don’t have it installed), open it in VS Code, navigate to that file, and press F5 to run that file in the debugger. Or just use the stackblitz example from earlier.

--

--

AlexAegis
The Startup

I mainly work with TypeScript, RxJS and Angular.