skip to content

In RxJS, what is the difference between merge() and concat() when you combine two observables into one stream?

level: juniorimportance: should knowfreq 58%

answer

  1. all at once, or one after another
  2. interleaved versus source order
  3. concat waits for completion
  4. merge's trailing concurrent number

basics

~10 s

merge subscribes to every source at once and forwards values as they arrive, interleaved; concat subscribes to the next source only after the previous one completes, so values come out source by source.

solid answer

~40 s

`merge(a$, b$)` subscribes to both immediately and emits values in whatever order they arrive; it completes when **all** sources have completed and errors as soon as any source errors. `concat(a$, b$)` subscribes to `a$` first, forwards everything it emits, and only when `a$` **completes** subscribes to `b$`. The output is therefore ordered source by source — but if `a$` never completes, `b$` is never subscribed at all. The RxJS docs describe `concat` as `merge` with its optional trailing `concurrent` argument set to `1`. Inside a `pipe`, the current forms are `mergeWith` and `concatWith`; the pipeable `merge` and `concat` operators are deprecated in RxJS 7.

code

ts · 9 lines
ts
import { concat, of, Observable } from 'rxjs';

interface Report { rows: number }

declare const cachedReport: Report;
declare const fetchReport$: Observable<Report>; // emits once, then completes

// Show the cached report immediately, then replace it with the fresh one.
const report$ = concat(of(cachedReport), fetchReport$);

go deeper

for a junior

Recall the one-line difference: merge runs all sources together and interleaves; concat runs them one after another, each waiting for the previous to complete.

for a middle

Explain completion and error rules for both, the concurrent argument, why concat equals merge with concurrency one, and the mergeWith and concatWith pipeable forms.

for a senior

Spot the traps in real pipelines: a never-completing first source stalling concat, a hot source losing values while waiting, and unbounded merge concurrency hammering a backend.

for a principal

Frame the choice as an ordering-versus-latency contract, and decide where a pipeline should bound concurrency rather than leaving it to each call site.

## Two static functions, two subscription policies `merge` and `concat` are **static creation functions** exported from `'rxjs'`: each takes several observables and returns one observable that re-emits their values. Neither transforms values or pairs them together — every value from every source comes out unchanged, one at a time. What differs is **when each source is subscribed**, and that decides the output order. - **`merge`** subscribes to all sources immediately. Output order is arrival order. - **`concat`** subscribes to one source at a time, moving to the next only when the current one completes. Output order is source order. ## merge in detail - Values are forwarded the instant they arrive, so two sources' values **interleave** by time. - The result completes only when **every** source has completed. - If any source errors, the error is forwarded and the remaining sources are unsubscribed. - A trailing number caps how many sources run at once: `merge(a$, b$, c$, 2)` subscribes to `a$` and `b$`, and subscribes to `c$` only when one of them completes. Without it the limit is unbounded. Typical uses: combining several event streams that mean the same thing (a refresh button, a keyboard shortcut and a periodic timer that all trigger a reload), or running a handful of independent side effects at once. ## concat in detail 1. Subscribe to the first source and forward its values. 2. When it completes, subscribe to the second source. 3. Repeat until the last source completes; then the result completes. 4. If any source errors, the result errors and the sources after it are never subscribed. Because later sources are subscribed late, `concat` has two classic traps: - **A never-completing source blocks everything after it.** `concat(interval(1000), b$)` never reaches `b$`. - **A hot source subscribed late misses what it already emitted.** If `b$` is a `Subject` that emitted while `a$` was still running, those values are gone by the time `concat` subscribes to it. Typical uses: emit a cached or placeholder value and then the fresh one (`concat(of(cached), fresh$)`), or run a fixed sequence of steps strictly one after another. ## Side by side | | `merge(a$, b$)` | `concat(a$, b$)` | |---|---|---| | Subscribes to `b$` | immediately | after `a$` completes | | Output order | by arrival time | all of `a$`, then all of `b$` | | Completes when | every source completes | the last source completes | | `a$` never completes | `b$` still flows | `b$` is never subscribed | | Concurrency | unbounded, or the trailing number | always one | | Pipeable form (RxJS 7) | `a$.pipe(mergeWith(b$))` | `a$.pipe(concatWith(b$))` | The docs make the relationship explicit: `concat` is equivalent to `merge` with `concurrent` set to `1` — with one source allowed at a time, the next one can only start when the current one finishes. ```ts import { concat, merge, map, take, timer } from 'rxjs'; const slow$ = timer(0, 300).pipe(map((i) => `slow${i}`), take(2)); const fast$ = timer(0, 100).pipe(map((i) => `fast${i}`), take(3)); merge(slow$, fast$).subscribe((v) => console.log('merge', v)); // slow0 fast0 fast1 fast2 slow1 (interleaved by time) concat(slow$, fast$).subscribe((v) => console.log('concat', v)); // slow0 slow1 fast0 fast1 fast2 (fast$ starts only after slow$ completes) ``` ## Static function or operator Inside a `pipe`, use the `With` operators: `a$.pipe(mergeWith(b$))` behaves like `merge(a$, b$)`, and `a$.pipe(concatWith(b$))` like `concat(a$, b$)`. The older pipeable `merge` and `concat` operators still exist in `'rxjs/operators'` but are deprecated in RxJS 7 and slated for removal in v8, because their names clash with the static functions. ## Not to be confused with `mergeMap` and `concatMap` also use the words merge and concat, but they map **each value** of one stream to a new inner observable and flatten the results. `merge` and `concat` join a **fixed list** of sources you already have.

  • What does the trailing number in merge(a$, b$, c$, 2) do?
    It caps concurrency: `merge` subscribes to at most two sources at a time. Here `a$` and `b$` start immediately and `c$` is subscribed only when one of them completes. Without the number the limit is unbounded. With the number set to `1` the behaviour is exactly `concat`.
  • Why can concat(a$, b$) lose values from b$ even when a$ does complete?
    `concat` subscribes to `b$` only after `a$` completes. If `b$` is hot — a `Subject` or an event stream that emits whether or not anyone listens — any value it emitted while `a$` was still running had no subscriber and is lost. A cold `b$` is unaffected, since its work starts on subscription.

saying these in an interview costs you the question

  • merge emits all of the first source's values before the second's
  • concat subscribes to every source up front and buffers the later ones
  • concat moves to the next source when the current one emits its first value
  • merge completes as soon as any one of its sources completes
  • concat(a$, b$) still reaches b$ even if a$ never completes