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 — transforming values
Section titled “map — transforming values”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 — handling failures
Section titled “catchError — handling failures”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:
- Takes a value from the source Observable
- Creates a new Observable from it (an inner Observable)
- 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 stringswitchMap(query => ...)subscribes to the search Observable. If a new query arrives while the search is still running, the old subscription is cancelledEMPTYis an Observable that immediately completes without emitting — used when the query is blank or when an error occurscatchError(() => EMPTY)inside theswitchMapswallows search errors without terminating the outer stream
Why switchMap matters
Section titled “Why switchMap matters”Without switchMap, if the user types “sci” then quickly changes to “action”:
- Request for “sci” starts
- Request for “action” starts
- “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.
The operator pipeline
Section titled “The operator pipeline”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.
Exercise
Section titled “Exercise”- Build a practice search component that reads from a text input using
fromEventorvalueChanges. - Chain
debounceTime(300),distinctUntilChanged(), andswitchMapto search a mock array. - Add
catchErrorthat returnsof([])on any error. - 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 — useof([])for empty arrays.switchMap(fn)subscribes to a new Observable per emission and cancels the previous one — essential for search.EMPTYis 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.