Skip to content

RxJS Operators in Angular

Operators are the building blocks you compose inside .pipe() to transform, filter, and combine Observable streams. This lesson covers the operators you will reach for most often in Angular apps.

What Are RxJS Operators?

An operator is a pure function that takes an Observable and returns a new Observable with some transformation applied, filtering values, mapping them to something else, or combining them with another stream, without mutating the original source.

Operators are chained together using .pipe(), reading top to bottom as the sequence of transformations a value goes through on its way to your subscriber.

searchInput$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  filter(term => term.length > 1),
  switchMap(term => this.searchService.search(term)),
  catchError(() => of([]))
).subscribe(results => this.results = results);

This pipeline debounces typing, ignores duplicate terms, skips very short queries, cancels stale searches, and gracefully falls back to an empty list on error.

Operator Categories

map, filter, tap                      // transform/inspect values
debounceTime, distinctUntilChanged     // control timing/duplicates
switchMap, mergeMap, concatMap, exhaustMap  // flatten inner Observables
combineLatest, forkJoin, merge          // combine multiple streams
catchError, retry                        // error handling
  • map/filter work like their array equivalents, but over time instead of over a fixed collection.
  • The *Map family (switchMap, mergeMap, concatMap, exhaustMap) each flatten inner Observables with different concurrency rules.
  • Combination operators (combineLatest, forkJoin, merge) merge multiple streams together in different ways.
  • catchError/retry handle failures within the pipeline itself.

RxJS Operators Cheatsheet

The operators you will use most in real Angular applications.

Operator Purpose
map Transform each emitted value
filter Only let values passing a predicate through
tap Perform a side effect without changing the value
debounceTime(ms) Wait for a pause in emissions before emitting
distinctUntilChanged() Skip consecutive duplicate values
switchMap Flatten and cancel any previous inner Observable
mergeMap Flatten and run inner Observables concurrently
concatMap Flatten inner Observables sequentially, one at a time
catchError Recover from or transform an error
take(n) / takeUntilDestroyed() Limit how long a subscription stays active

switchMap vs mergeMap vs concatMap vs exhaustMap

These four operators all flatten an Observable-of-Observables into a single stream, but differ in how they handle overlapping inner Observables, a distinction that matters a lot for correctness in real apps.

Operator Behavior When a New Value Arrives
switchMap Cancels the previous inner Observable and switches to the new one
mergeMap Keeps the previous inner Observable running, runs both concurrently
concatMap Queues the new inner Observable until the current one finishes
exhaustMap Ignores new values entirely while an inner Observable is still active

Example: A Debounced Search Box

This combination, debounceTime plus distinctUntilChanged plus switchMap, is one of the most common RxJS patterns in real Angular applications, and worth memorizing as a unit.

@Component({ selector: 'app-search', standalone: true, imports: [ReactiveFormsModule], template: '...' })
export class SearchComponent {
  search = new FormControl('');
  results$ = this.search.valueChanges.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term => this.searchService.search(term ?? ''))
  );
}

Choosing the Right Flattening Operator

Use switchMap for "only the latest request matters" scenarios (search, live filters). Use concatMap when order and completeness matter (processing a queue of uploads one at a time). Use exhaustMap to prevent duplicate submissions, like ignoring extra clicks on a submit button while a request is still in flight.

Common Mistakes

  • Using mergeMap for a search box, allowing older, stale requests to resolve after (and overwrite) a newer one.
  • Using switchMap for a form submission handler, accidentally canceling an in-progress save if the user clicks submit twice.
  • Forgetting catchError inside a pipeline, letting a single failed emission terminate the whole subscription instead of a single request.
  • Overcomplicating a simple synchronous transformation with RxJS operators when a plain array .map()/.filter() would do.

Key Takeaways

  • Operators are composed inside .pipe() to transform, filter, and combine Observable streams.
  • switchMap, mergeMap, concatMap, and exhaustMap each flatten inner Observables with different concurrency semantics.
  • debounceTime + distinctUntilChanged + switchMap is the standard pattern for reactive search-as-you-type.
  • Choosing the wrong flattening operator is a common source of subtle race-condition bugs.

Pro Tip

When in doubt about which flattening operator to use, default to switchMap for read operations (searches, lookups) and exhaustMap for write operations you want to protect from duplicate submissions (like form saves).