skip to content

In RxJS, how do switchMap, mergeMap, concatMap and exhaustMap differ when a new value arrives while an inner observable is still running?

level: middleimportance: must knowfreq 82%

answer

  1. what happens to the busy inner
  2. cancel, run alongside, queue, ignore
  3. concatMap is mergeMap with one slot
  4. search, save, login

basics

~20 s

switchMap unsubscribes the running inner observable and switches to the new one; mergeMap runs both at once; concatMap queues the new value until the current inner completes; exhaustMap ignores the new value until the current inner completes.

solid answer

~50 s

All four map each source value to an inner observable and flatten the results into one stream; they differ only in what happens when a value arrives while an inner is still active. `switchMap` **cancels** the active inner (unsubscribes) and subscribes to the new one — right for a typeahead, where only the latest query matters. `mergeMap` **runs them concurrently**, optionally capped by a `concurrent` argument, so results can arrive out of order. `concatMap` **queues** the value and starts its inner only after the previous one completes — in RxJS 7.8 it is literally `mergeMap(project, 1)` — right for saves that must happen in order. `exhaustMap` **drops** the value if an inner is active — right for a login button that must not submit twice. All four complete only when the source has completed and no inner remains active.

code

ts · 10 lines
ts
import { of, delay, switchMap, mergeMap, concatMap, exhaustMap, Observable } from 'rxjs';

const work = (id: string): Observable<string> => of(`done ${id}`).pipe(delay(100));

const source$ = of('a', 'b');

source$.pipe(switchMap(work)).subscribe(console.log);  // done b
source$.pipe(mergeMap(work)).subscribe(console.log);   // done a, done b (together)
source$.pipe(concatMap(work)).subscribe(console.log);  // done a, then done b 100 ms later
source$.pipe(exhaustMap(work)).subscribe(console.log); // done a

go deeper

for a junior

Recall the four verbs: switch cancels, merge runs together, concat queues, exhaust ignores, and name one use case for each.

for a middle

Explain the mechanics: unsubscribing the previous inner, the concurrent limit and buffer behind concatMap, and completion only after the last inner finishes.

for a senior

Choose per use case and defend it, including why switchMap on writes is risky and how an inner error kills the outer stream.

for a principal

Frame the choice as a product decision about stale results, ordering and duplicate submissions, and make it consistent across the codebase.

## The shared shape A **higher-order mapping operator** takes a `project` function that turns each source value into an **inner observable** (or any `ObservableInput`: a promise, an array…), subscribes to it, and forwards the inner values into a single output stream. The four operators differ in one decision: **what to do with a new source value while an inner subscription is still active**. | operator | new value while an inner is active | output ordering | typical use | |---|---|---|---| | `switchMap` | unsubscribe the active inner, start the new one | only the latest inner's values | typeahead search, route-driven loads | | `mergeMap` | start another inner alongside (up to `concurrent`) | interleaved, by arrival | independent requests, uploads | | `concatMap` | buffer the value, start it when the active inner completes | strictly in source order | ordered saves, sequential writes | | `exhaustMap` | ignore the value | first-come; later ones are dropped | login or pay button, non-overlapping polling | ## How RxJS 7.8 implements them - **`switchMap(project)`** keeps one inner subscriber. On every source value it calls `unsubscribe()` on the previous one before subscribing to the new inner. - **`mergeMap(project, concurrent = Infinity)`** counts active inners. Below the limit, it subscribes immediately; at the limit, it pushes the value into an **unbounded buffer** and starts it when a slot frees. - **`concatMap(project)`** is implemented as `mergeMap(project, 1)`: one slot, so every other value waits in the buffer, in order. - **`exhaustMap(project)`** keeps one inner subscriber. If one is active, the source value is simply not projected — nothing is buffered. The `project` function also receives an index, `(value, index)`, counting source values. ## Completion and errors Each operator completes only when **the source has completed and no inner is active** (and, for `mergeMap`/`concatMap`, the buffer is empty). A source that completes while a request is in flight therefore still delivers that request's result. An error from **any** inner observable errors the whole output and tears down the source subscription. A search box whose request fails once stops working unless the inner observable handles its own errors. ## Choosing in practice 1. **Only the newest result matters** → `switchMap`. Stale work is cancelled; with Angular's `HttpClient`, unsubscribing aborts the underlying request. 2. **Every result matters and order does not** → `mergeMap`, with a `concurrent` limit if the server or browser should not see unlimited parallel requests. 3. **Every result matters and order does** → `concatMap`; watch the buffer if values arrive faster than inners complete. 4. **A new trigger should be ignored while work is in progress** → `exhaustMap`. A common trap is using `switchMap` for **writes**: cancelling a save on the client does not guarantee the server did not already process it, and the user's earlier change may silently win or lose. ## Misconceptions to avoid - "`switchMap` waits for the current inner to finish" — it cancels it; waiting is `concatMap`. - "`exhaustMap` queues the ignored values" — nothing is stored; the values are gone. - "`mergeMap` keeps results in source order" — results interleave by arrival time. - "`concatMap` is always safe" — its buffer has no limit, so a fast source and slow inners build an ever-growing backlog. - "the choice only matters for HTTP" — the same rules apply to timers, animations, WebSocket streams or any other inner observable. ## A worked timeline Source values `a` at 0 ms and `b` at 50 ms; each inner emits once, 100 ms after it starts. | operator | emissions | |---|---| | `switchMap` | `b` result at 150 ms (`a` was cancelled at 50 ms) | | `mergeMap` | `a` result at 100 ms, `b` result at 150 ms | | `concatMap` | `a` result at 100 ms, `b` result at 200 ms | | `exhaustMap` | `a` result at 100 ms (`b` was ignored) | The `*All` operators — `switchAll`, `mergeAll`, `concatAll`, `exhaustAll` — apply the same four strategies to a stream that already emits observables.

  • Why is concatMap described as mergeMap with a concurrency of one?
    In RxJS 7.8, `concatMap(project)` is implemented as `mergeMap(project, 1)`. With one slot, each new value waits in mergeMap's buffer until the active inner completes, which is exactly sequential, ordered execution.
  • When does switchMap's output complete?
    Only when the source has completed and the currently active inner has completed too. A source that completes while the last request is in flight still lets that request's values through before completion.
  • What happens to the whole stream if one inner request errors?
    The error propagates to the output and tears down the source subscription, so no further values are mapped. The inner observable must handle its own errors if the stream should survive a failed request.

A receptionist taking calls: switchMap hangs up on the current caller when a new one rings, mergeMap puts everyone on a conference line, concatMap puts new callers on hold in order, and exhaustMap lets the phone ring out while busy.

saying these in an interview costs you the question

  • switchMap waits for the current request to finish before starting the next.
  • concatMap drops values that arrive while an inner is running.
  • exhaustMap queues clicks and replays them once the inner completes.
  • mergeMap preserves source order in its output.
  • switchMap is the safe default for saving data.