These RxJS flattening operators control how Angular handles overlapping async work (HTTP calls, saves, clicks, search). switchMap cancels previous, mergeMap runs in parallel, concatMap queues sequentially, and exhaustMap ignores new triggers while busy. Choosing the right one prevents race conditions, double submits, and UI glitches.
Frontend interview practice question
switchMap vs mergeMap vs exhaustMap vs concatMap: When Would You Use Each in Angular?
Interview quick answer
Interview focus
This Angular interview question tests whether you can explain switchMap vs mergeMap vs concatMap vs exhaustMap in Angular, connect it to production trade-offs, and handle common follow-up questions.
- switchMap vs mergeMap vs concatMap vs exhaustMap in Angular explanation without falling back to memorized definitions
- RxJS and Operators reasoning, edge cases, and production failure modes
- How you would answer the most likely Angular interview follow-up
Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
Full interview answer
Core ideaswitchMap, mergeMap, concatMap, and exhaustMap are RxJS flattening operators, and in Angular they decide what happens when overlapping HTTP calls, saves, or clicks compete. All four operators take a stream of “triggers” (typing, clicks, route params) and map each trigger to an inner Observable (usually an HTTP call). The difference is what they do when a new trigger arrives before the previous one finishes:switchMap = cancel old, keep latestmergeMap = run all in parallelconcatMap = queue one by oneexhaustMap = ignore new until current completes
Operator | What it does with overlap | Best Angular use-cases | Common bug it prevents (or causes) |
|---|---|---|---|
| Cancels the previous inner Observable when a new value arrives (keeps only the latest). | Search/autocomplete, typeahead filtering, route param changes where you only want the latest request. | Prevents stale UI from older responses. (But can cancel work you actually needed.) |
| Runs all inner Observables concurrently (no cancellation). | Fire-and-forget analytics, loading independent resources in parallel, batch requests where order doesn’t matter. | Can cause race conditions (older response overwrites newer state) if you update shared UI state. |
| Queues triggers and runs inner Observables sequentially (preserves order). | Save queue, uploading files in order, sequential steps where order matters and every action must run. | Prevents out-of-order state. (But can feel “slow” under rapid triggers.) |
| Ignores new triggers while an inner Observable is active (first wins until completion). | Login button, “Submit” button, “Pay now”, any action where double submit must be blocked. | Prevents double submits. (But can drop user actions if they click again too soon.) |
switchMap vs exhaustMap
Verdict: both handle a new trigger that arrives while the previous inner Observable is still running, but in opposite ways. switchMap cancels the running one and starts the new one (latest wins); exhaustMap keeps the running one and drops the new trigger (first wins until it completes).
Pick: switchMap for search, filters, and route params where only the newest result matters; exhaustMap for login, submit, and pay buttons where a second click must not start a second request.
Timeline: click A at 0 ms, click B at 100 ms, each request takes 300 ms. switchMap unsubscribes A at 100 ms and the UI gets only B at 400 ms. exhaustMap ignores B and the UI gets only A at 300 ms.
switchMap vs mergeMap
Verdict: switchMap keeps at most one inner Observable alive and cancels the previous one; mergeMap keeps every inner Observable alive and lets responses arrive in any order.
Pick: switchMap when the UI must show the latest request only; mergeMap for independent, fire-and-forget work such as analytics events or loading unrelated resources in parallel.
Timeline: with the same clicks, mergeMap runs A and B together. If A responds after B, A overwrites the newer result, which is the classic stale search bug. switchMap cannot produce that overwrite because A was already unsubscribed.
switchMap vs concatMap
Verdict: switchMap throws away in-flight work when a new trigger arrives; concatMap never throws anything away and runs triggers one after another in arrival order.
Pick: switchMap for reads where only the newest value matters; concatMap for writes that must all complete in order, such as saving the steps of a wizard or queued commands.
Timeline: with the same clicks, concatMap starts B only after A completes at 300 ms, so B finishes at 600 ms and both saves land in order. switchMap would have canceled the A save, which is data loss for a write.
Event | switchMap | mergeMap | concatMap | exhaustMap |
|---|---|---|---|---|
0 ms: trigger A | starts A | starts A | starts A | starts A |
100 ms: trigger B while A runs | cancels A, starts B | starts B, A keeps running | queues B behind A | ignores B |
300 ms: A would complete | nothing (A was unsubscribed) | A result arrives | A completes, B starts | A completes |
Result in the UI | B only | A and B in response order | A then B, in order | A only |
Interview mental model
Ask yourself: when triggers happen fast, do you want latest wins, all run, in order, or ignore spam?
You want… | Pick | Why |
|---|---|---|
Only the latest result (cancel previous) |
| Typing fast should not show older results. |
All requests to run concurrently |
| Independent work can finish in any order. |
Every action to run, but strictly in order |
| Queue saves/commands to avoid state corruption. |
Block spam clicks until completion |
| Prevent double submit / duplicate payments. |
Real Angular examples
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged, switchMap, mergeMap, concatMap, exhaustMap, tap, filter } from 'rxjs/operators';
import { Observable, of, Subject } from 'rxjs';
class Api {
search(term: string): Observable<string[]> { return of([term]); }
save(payload: unknown): Observable<void> { return of(void 0); }
analytics(event: string): Observable<void> { return of(void 0); }
}
@Component({
selector: 'app-demo',
template: ``,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DemoComponent {
private api = inject(Api);
// 1) Typeahead search: latest wins => switchMap
searchControl = new FormControl('');
searchResults$ = this.searchControl.valueChanges.pipe(
debounceTime(250),
distinctUntilChanged(),
switchMap(term => this.api.search(String(term ?? '')))
);
// 2) Submit button: block double submit => exhaustMap
private submitClicks$ = new Subject<void>();
submitting$ = this.submitClicks$.pipe(
exhaustMap(() => this.api.save({ /* form value */ }))
);
// 3) Sequential saves: preserve order => concatMap
private saveQueue$ = new Subject<unknown>();
saveInOrder$ = this.saveQueue$.pipe(
concatMap(payload => this.api.save(payload))
);
// 4) Parallel work: independent calls => mergeMap
private events$ = new Subject<string>();
trackEvents$ = this.events$.pipe(
mergeMap(name => this.api.analytics(name))
);
}
Ordered intent example: when concatMap beats switchMap
If the user clicks “Save step 1”, then “Save step 2”, then “Save step 3”, those intents must usually stay ordered. switchMap would cancel earlier saves; concatMap preserves the queue.
saveDraftClicks$.pipe(
concatMap(stepPayload => this.api.saveDraft(stepPayload))
);
// Use this when every save matters and order is part of correctness.
Same trigger, four different policies
Imagine the exact same click stream hitting four operator choices:switchMap = keep the latest clickmergeMap = let every click run in parallelconcatMap = queue every click in orderexhaustMap = ignore later clicks while busy
That is why this question is really about user-intent policy, not memorizing operators.
Senior pitfall #1: race conditions with mergeMap
If you use mergeMap for UI state that should reflect the latest request, you can get out-of-order overwrites: request A starts, then request B starts, B returns first (UI shows B), then A returns later and overwrites the UI with stale data. That’s why typeahead/search is almost always switchMap.
Senior pitfall #2: thinking switchMap “cancels the server”switchMap unsubscribes from the previous inner Observable. For Angular HttpClient, unsubscribing typically aborts the underlying request (via browser APIs). But if the server already processed it, cancellation won’t rewind time—it mainly prevents your app from handling stale results.
Senior pitfall #3: exhaustMap dropping user intentexhaustMap ignores new triggers while busy. Great for preventing double submits, but if you need “last click wins after current finishes”, use a different strategy (e.g., disable button in UI, or use switchMap with careful semantics).
Non-HTTP note
These operators are not HTTP-only. The same choice matters for router params, WebSocket reconnect commands, drag streams, and any long-lived inner Observable where overlap policy changes correctness.
Angular scenario | Best operator | Reason |
|---|---|---|
Search field / autocomplete |
| Only latest input should win; old requests must be ignored. |
Submit / payment / login button |
| Block duplicates until completion. |
Queueing writes (save draft events, ordered commands) |
| Guarantees order and prevents overlapping state changes. |
Load independent resources (A, B, C in parallel) |
| Parallelism improves speed; ordering doesn’t matter. |
Summary
switchMap = latest wins (search, route changes). mergeMap = run all in parallel (independent work). concatMap = queue sequentially (ordered saves). exhaustMap = ignore spam until done (submit buttons). The “correct” choice is the one that matches how you want to handle overlapping triggers.
Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.