Skip to content

RxJS Operators: map, catchError, switchMap

RxJS operators transform Observables. You chain them with .pipe(), passing the Observable’s value through each operator in sequence. Three operators appear throughout CinemaVault: map, catchError, and switchMap.

map transforms each emitted value, like Array.prototype.map but for Observables:

import { map } from 'rxjs';
getPopular(): Observable<Movie[]> {
return this.http.get<MoviesResponse>(url, { params })
.pipe(
map(response => response.results)
);
}

The Observable emits a MoviesResponse. After map, subscribers receive Movie[] — just the array they need.

You can chain multiple map operators:

.pipe(
map(response => response.results),
map(movies => movies.filter(m => m.poster_path !== null))
)

catchError intercepts errors in the Observable stream and lets you handle them gracefully:

import { map, catchError, of } from 'rxjs';
getPopular(): Observable<Movie[]> {
return this.http.get<MoviesResponse>(url, { params })
.pipe(
map(response => response.results),
catchError(error => {
console.error('Failed to fetch movies:', error);
return of([]); // Return an empty array on error
})
);
}

of([]) creates an Observable that immediately emits an empty array. The subscriber receives [] instead of an error, and the UI can show an empty state instead of breaking.

Without catchError, an HTTP error propagates to the subscriber’s error callback. With it, you can decide what to emit instead.

switchMap — canceling in-flight requests

Section titled “switchMap — canceling in-flight requests”

switchMap is the most powerful of the three. It:

  1. Takes a value from the source Observable
  2. Creates a new Observable from it (an inner Observable)
  3. Cancels the previous inner Observable if a new value arrives before it completes

This is perfect for search: if the user types quickly, only the last request should matter.

In SearchResults, query parameters come from ActivatedRoute.queryParamMap. When the user submits a new search, the query param changes and a new API call should start — canceling any previous one:

import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { switchMap, map, catchError, EMPTY } from 'rxjs';
@Component({ /* ... */ })
export class SearchResults implements OnInit {
private route = inject(ActivatedRoute);
private movieService = inject(MovieService);
results: Movie[] = [];
totalResults = 0;
query = '';
ngOnInit(): void {
this.route.queryParamMap.pipe(
map(params => params.get('q') ?? ''),
switchMap(query => {
this.query = query;
if (!query) return EMPTY;
return this.movieService.search(query).pipe(
catchError(() => EMPTY)
);
})
).subscribe(response => {
this.results = response.results;
this.totalResults = response.total_results;
});
}
}

Key details:

  • map(params => params.get('q') ?? '') extracts the query string
  • switchMap(query => ...) subscribes to the search Observable. If a new query arrives while the search is still running, the old subscription is cancelled
  • EMPTY is an Observable that immediately completes without emitting — used when the query is blank or when an error occurs
  • catchError(() => EMPTY) inside the switchMap swallows search errors without terminating the outer stream

Without switchMap, if the user types “sci” then quickly changes to “action”:

  1. Request for “sci” starts
  2. Request for “action” starts
  3. “sci” response arrives (late) and overwrites “action” results

With switchMap, when “action” is typed, the “sci” request is automatically cancelled, and only the “action” response matters.

this.route.queryParamMap.pipe(
map(params => params.get('q') ?? ''), // 1. extract query
switchMap(query => { // 2. cancel old, start new
if (!query) return EMPTY;
return this.movieService.search(query);
})
).subscribe(response => { // 3. receive results
this.results = response.results;
});

Read .pipe() chains from top to bottom: each operator receives the output of the previous one.

  1. Build a practice search component that reads from a text input using fromEvent or valueChanges.
  2. Chain debounceTime(300), distinctUntilChanged(), and switchMap to search a mock array.
  3. Add catchError that returns of([]) on any error.
  4. Log each emission to trace the flow through the operators.
  • map(fn) transforms each emitted value — use it to extract data from API response wrappers.
  • catchError(fn) intercepts errors and replaces them with a recovery Observable — use of([]) for empty arrays.
  • switchMap(fn) subscribes to a new Observable per emission and cancels the previous one — essential for search.
  • EMPTY is an Observable that completes immediately without emitting — use it to short-circuit without errors.
  • Chain operators with .pipe(operator1, operator2, ...) — each receives the output of the previous.