TL;DR
- Use
map
for data transformation, not for creating new observables. - Use
switchMap
for latest-only results (search, filters) to cancel previous requests. - Use
concatMap
for sequential, ordered processing (multi-step forms, queues). - Use
exhaustMap
to prevent duplicate actions (form submissions, button spam). - Avoid nested subscriptions and memory leaks by using the right RxJS operator for your scenario.
Why do so many Angular developers misuse switchMap
? You’ll see it everywhere, search components that fire duplicate requests, form submissions that create race conditions, and data streams that behave unpredictably under load.
The problem isn’t complexity. It’s that most RxJS explanations focus on marble diagrams instead of real Angular scenarios. You need to see these operators solving actual problems in your components and services.
Let’s walk through map
, switchMap
, concatMap
, and exhaustMap
with production-ready Angular code that you can use today.
Understanding the Core Difference
Before diving into examples, here’s the key distinction: map
transforms values, while the other three handle inner observables, streams created from each emitted value.
// map: transforms each value
source$.pipe(map(value => value * 2))
// switchMap/concatMap/exhaustMap: creates new observables from each value
source$.pipe(switchMap(value => this.http.get(`/api/data/${value}`)))
map
: Transform Values Without Creating Observables
Use map
when you need to transform data without creating new observables. This works well for formatting API responses or adjusting data structure.
// user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers() {
return this.http.get<ApiUser[]>('/api/users').pipe(
map(users => users.map(user => ({
id: user.id,
fullName: `${user.firstName} ${user.lastName}`,
isActive: user.status === 'active',
avatar: user.avatarUrl || '/assets/default-avatar.png'
})))
);
}
}
// user-list.component.ts
@Component({
selector: 'app-user-list',
standalone: true,
template: `
<div *ngFor="let user of users$ | async">
<img [src]="user.avatar" [alt]="user.fullName">
<span [class.inactive]="!user.isActive">{{ user.fullName }}</span>
</div>
`
})
export class UserListComponent {
private userService = inject(UserService);
users$ = this.userService.getUsers();
}
The map
operator transforms the raw API response into a clean UI model. Each user object gets restructured, but no new HTTP requests are created.
switchMap
: Cancel Previous Requests
switchMap
cancels the previous inner observable when a new value arrives. This works perfectly for search boxes and live filtering where you only care about the latest request.
// search.service.ts
@Injectable({ providedIn: 'root' })
export class SearchService {
private http = inject(HttpClient);
private searchSubject = new BehaviorSubject<string>('');
search$ = this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => {
if (!query.trim()) {
return of([]);
}
return this.http.get<Product[]>(`/api/products/search?q=${query}`);
})
);
updateSearch(query: string) {
this.searchSubject.next(query);
}
}
// search.component.ts
@Component({
selector: 'app-search',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<input
[(ngModel)]="searchTerm"
(input)="onSearch($event)"
placeholder="Search products...">
<div *ngFor="let product of results$ | async">
{{ product.name }} - ${{ product.price }}
</div>
`
})
export class SearchComponent {
private searchService = inject(SearchService);
searchTerm = '';
results$ = this.searchService.search$;
onSearch(event: Event) {
const query = (event.target as HTMLInputElement).value;
this.searchService.updateSearch(query);
}
}
Here’s what happens: when a user types “iPhone” then quickly changes to “Android”, switchMap
cancels the “iPhone” request if it’s still pending. Only the “Android” results will display.
concatMap
: Preserve Request Order
concatMap
waits for each inner observable to complete before processing the next one. Use this when order matters, like saving form steps or processing a queue.
// form-wizard.service.ts
@Injectable({ providedIn: 'root' })
export class FormWizardService {
private http = inject(HttpClient);
private saveQueue = new Subject<FormStep>();
// Process saves in order, one at a time
saveResults$ = this.saveQueue.pipe(
concatMap(step =>
this.http.post<SaveResponse>(`/api/wizard/step/${step.id}`, step.data).pipe(
tap(response => console.log(`Step ${step.id} saved:`, response)),
catchError(error => {
console.error(`Step ${step.id} failed:`, error);
return of({ success: false, stepId: step.id });
})
)
)
);
queueSave(step: FormStep) {
this.saveQueue.next(step);
}
}
// wizard.component.ts
@Component({
selector: 'app-wizard',
standalone: true,
template: `
<div *ngFor="let step of steps; let i = index">
<h3>Step {{ i + 1 }}</h3>
<button (click)="saveStep(step)" [disabled]="saving">
Save Step
</button>
</div>
<div *ngIf="saveResults$ | async as result">
Save result: {{ result.success ? 'Success' : 'Failed' }}
</div>
`
})
export class WizardComponent {
private formService = inject(FormWizardService);
saving = false;
steps: FormStep[] = [
{ id: 1, data: { name: 'Basic Info' } },
{ id: 2, data: { address: 'Contact Details' } },
{ id: 3, data: { preferences: 'Settings' } }
];
saveResults$ = this.formService.saveResults$;
saveStep(step: FormStep) {
this.formService.queueSave(step);
}
}
With concatMap
, if you click “Save Step” on steps 1, 2, and 3 rapidly, they’ll save in order: step 1 completes, then step 2 starts, then step 3. No race conditions.
exhaustMap
: Ignore Duplicate Submissions
exhaustMap
ignores new values while the current inner observable is still active. This prevents duplicate submissions and button spam.
// order.service.ts
@Injectable({ providedIn: 'root' })
export class OrderService {
private http = inject(HttpClient);
private submitSubject = new Subject<OrderData>();
// Ignore clicks while order is processing
submitOrder$ = this.submitSubject.pipe(
exhaustMap(orderData =>
this.http.post<OrderResponse>('/api/orders', orderData).pipe(
tap(response => console.log('Order submitted:', response.orderId)),
catchError(error => {
console.error('Order failed:', error);
return of({ success: false, error: error.message });
})
)
)
);
submitOrder(orderData: OrderData) {
this.submitSubject.next(orderData);
}
}
// checkout.component.ts
@Component({
selector: 'app-checkout',
standalone: true,
template: `
<form (ngSubmit)="onSubmit()">
<input [(ngModel)]="orderData.customerName" placeholder="Name">
<input [(ngModel)]="orderData.total" type="number" placeholder="Total">
<button type="submit" [disabled]="isSubmitting">
{{ isSubmitting ? 'Processing...' : 'Place Order' }}
</button>
</form>
<div *ngIf="orderResult$ | async as result">
{{ result.success ? 'Order placed!' : 'Error: ' + result.error }}
</div>
`
})
export class CheckoutComponent {
private orderService = inject(OrderService);
isSubmitting = false;
orderData: OrderData = { customerName: '', total: 0 };
orderResult$ = this.orderService.submitOrder$;
constructor() {
// Track submission state
this.orderResult$.subscribe(() => {
this.isSubmitting = false;
});
}
onSubmit() {
this.isSubmitting = true;
this.orderService.submitOrder(this.orderData);
}
}
Even if users click “Place Order” five times rapidly, exhaustMap
ensures only the first click triggers an HTTP request. Subsequent clicks are ignored until the order completes.
Operator Timing Behavior
Here’s how each operator handles multiple rapid emissions:

RxJS operator timing comparison: map processes each value independently, switchMap cancels previous requests for new ones, concatMap queues requests sequentially, and exhaustMap ignores new requests while one is active.
Comparison Table: When to Use Each Operator
Operator | Cancels Previous | Preserves Order | Best For | Common Gotcha |
---|---|---|---|---|
map | N/A | N/A | Data transformation | Creating nested observables accidentally |
switchMap | Yes | No | Search, live filters | Losing important requests |
concatMap | No | Yes | Sequential operations | Blocking on slow requests |
exhaustMap | No | No | Preventing duplicates | Missing valid user actions |
Common RxJS Pitfalls in Angular
Nested Subscriptions with map
:
// Wrong - creates nested subscriptions
getUserPosts() {
return this.getUser().pipe(
map(user => {
return this.http.get(`/api/posts/${user.id}`).subscribe(posts => {
// This creates a memory leak
});
})
);
}
// Right - use switchMap for dependent requests
getUserPosts() {
return this.getUser().pipe(
switchMap(user => this.http.get(`/api/posts/${user.id}`))
);
}
Double HTTP Calls with switchMap
:
// Wrong - triggers on every component init
ngOnInit() {
this.data$ = this.route.params.pipe(
switchMap(params => this.dataService.getData(params.id))
);
}
// Right - use shareReplay to cache results
ngOnInit() {
this.data$ = this.route.params.pipe(
switchMap(params => this.dataService.getData(params.id)),
shareReplay(1)
);
}
When to Use What: Mental Models
Think of these operators like different traffic management systems:
map
: Traffic light that changes the color of cars passing throughswitchMap
: Highway merge that cancels slow lanes when faster traffic arrivesconcatMap
: Single-file bridge where cars cross one at a time in orderexhaustMap
: Busy intersection that ignores new cars until current ones clear
Key Takeaways
Choose your RxJS operator based on what you need:
- Use
map
for transforming data without creating new observables - Use
switchMap
when you only care about the latest result (search, filters) - Use
concatMap
when order matters and you can’t afford to lose operations - Use
exhaustMap
when you need to prevent duplicate actions
The difference between success and frustration often comes down to picking the right operator for your specific use case. Master these four, and you’ll handle 90% of Angular RxJS scenarios with confidence.
Frequently Asked Questions
What is the difference between map
and switchMap
in Angular RxJS?
map
transforms each value emitted by an observable, while switchMap
creates a new inner observable for each value and cancels the previous one. Example:
// map: transforms values
source$.pipe(map(value => value * 2))
// switchMap: cancels previous HTTP request
source$.pipe(switchMap(value => this.http.get(`/api/data/${value}`)))
When should you use switchMap
in Angular?
switchMap
for scenarios like search boxes or live filters where only the latest result matters. It cancels previous HTTP requests if a new value arrives before completion, preventing duplicate or outdated results.What is concatMap
and when is it useful?
concatMap
queues inner observables and processes them one at a time in order. Use it when order matters, such as saving form steps or processing a queue of actions.How does exhaustMap
prevent duplicate submissions?
exhaustMap
ignores new emissions while the current inner observable is active. This is ideal for preventing duplicate form submissions or button spam, as only the first action is processed until completion.What are common pitfalls with map
and switchMap
in Angular?
map
to create nested subscriptions can cause memory leaks. Using switchMap
without caching (e.g., shareReplay
) can trigger duplicate HTTP calls on every subscription.How do you handle sequential HTTP requests in Angular?
concatMap
to ensure each HTTP request completes before the next starts. This is useful for workflows where order and completion are critical, such as multi-step forms.How can you avoid memory leaks with RxJS in Angular?
switchMap
, concatMap
, exhaustMap
) and the async pipe in templates to manage subscriptions automatically.What is a mental model for choosing between map
, switchMap
, concatMap
, and exhaustMap
?
map
for data transformation, switchMap
for latest-only results, concatMap
for ordered processing, and exhaustMap
to ignore duplicates. Think of them as different traffic management systems for your data flow.How do you prevent race conditions in Angular HTTP requests?
switchMap
to ensure only the latest request is processed, cancelling any previous pending requests. This is especially important in search or autocomplete scenarios.What is the best practice for handling dependent HTTP requests in Angular?
switchMap
to ensure the second request depends on the result of the first, avoiding nested subscriptions and memory leaks.Related Posts
- Angular Interceptor with Retry, Timeout & Unified Error Handling
- Stop Subscribing in Angular Components: Use Async Pipe + Guard Clauses Instead
- Angular Structural Directives Explained: ngIf vs @if and Creating Custom Directives
- Lazy Loading in Angular 19: Modules vs Standalone Components
- Angular Signals: A Hands-on Guide to Better State Management