
Race conditions are the nightmares of frontend development. They happen when your code assumes that network request A will finish before network request B, but the internet decides otherwise.
In React or Vue (without RxJS), managing complex concurrency often requires boolean flags (isLoading), timestamp checks, or AbortControllers.
In Angular, we solve this elegantly with RxJS Mapping Operators.
But RxJS gives us four different tools: mergeMap, switchMap, concatMap, and exhaustMap. Choosing the wrong one isn't just a style choice; it determines whether your app creates bugs or prevents them.
Let's break down exactly which concurrency strategy each operator uses.
1. switchMap: The "Latest and Greatest" (Fixing Read Races)
The Strategy: Cancel the previous request if a new one arrives.
The Scenario:
Imagine a user is on a "User Details" page.
- They click "User A". (Request A starts - Slow Network).
- They get bored and click "User B" immediately. (Request B starts - Fast Network).
The Bug (without switchMap):
Request B finishes first and shows User B. Then, Request A finishes and overwrites the screen with User A. The URL says "User B", but the screen shows "User A". This is a dirty UI state.
The Fix:
this.route.params.pipe(
// When params change, cancel any pending request!
switchMap(params => this.userService.getUser(params.id))
).subscribe(user => {
// We are GUARANTEED that 'user' matches the current URL.
this.displayUser(user);
});
Best For: GET requests (Search, Route Params, Filters).
2. concatMap: The "Single File Line" (Fixing Write Races)
The Strategy: Wait for the current request to finish before starting the next.
The Scenario:
An "Auto-Save" feature in a text editor.
- User types "Hello". (Request A sent).
- User immediately types " World". (Request B sent).
The Bug (with switchMap):
If you used switchMap, Request B would cancel Request A. The server only receives " World". The word "Hello" is lost forever.
The Fix:
We need a Queue. Request B must wait in line.
this.textInput.valueChanges.pipe(
// Queue the requests. Request B waits for Request A to complete.
concatMap(text => this.api.saveDraft(text))
).subscribe();
Best For: PUT/POST requests where order matters (Auto-saves, sequential steps).
3. mergeMap: The "Free-for-All" (Parallel Execution)
The Strategy: Run everything immediately. I don't care about order.
The Scenario:
A "Bulk Delete" feature. The user selects 5 items from a list and clicks "Delete Selected".
The Strategy:
We don't want to wait for Item 1 to delete before starting Item 2 (too slow). And we certainly don't want to cancel Item 1 if Item 2 starts (bug). We want them all to fly to the server at once.
The Fix:
const idsToDelete = [1, 2, 3, 4, 5];
from(idsToDelete).pipe(
// Fire 5 network requests in parallel
mergeMap(id => this.api.deleteItem(id))
).subscribe(deletedId => {
console.log(`Deleted ${deletedId}`);
});
Best For: DELETE requests or fetching unrelated data (e.g., getting Widget A and Widget B at the same time).
4. exhaustMap: The "Do Not Disturb" (Spam Prevention)
The Strategy: Ignore new requests while I am busy.
The Scenario:
A "Pay Now" button or a Login form.
A nervous user clicks the button 10 times in one second because the spinner didn't appear instantly.
The Bug (with mergeMap):
You charge the user's credit card 10 times.
The Bug (with switchMap):
You cancel the payment 9 times, and only the 10th one works (maybe).
The Fix:
We want to completely ignore clicks 2 through 10.
fromEvent(this.payButton, 'click').pipe(
// If a payment is processing, IGNORE this click.
exhaustMap(() => this.paymentService.chargeCard())
).subscribe();
Best For: Non-idempotent POST requests (Login, Payment, Submit).
Summary Cheat Sheet
| Operator | Concurrency Strategy | Ideal Use Case |
| switchMap | Kill the previous | Reads (Search, Routing) |
| concatMap | Wait in line | Ordered Writes (Save) |
| mergeMap | Run in parallel | Parallel Writes (Delete) |
| exhaustMap | Ignore the new | Submit Buttons (Auth) |
RxJS operators are tools for managing time. Master these four, and you master concurrency.