| |

Angular 39 🅰️ RxJS Fundamentals — Observables

RxJS is the reactive layer underneath Angular. Every HTTP call, every router event, every form value change, and every signal-based computation that Angular exposes is an Observable underneath. An Observable is a description of a stream — a sequence of values that arrive over time. Unlike a Promise, which produces exactly one value and resolves once, an Observable can produce zero, one, or many values, and it can be canceled. The distinction matters because almost everything in a front-end application is a stream: keystrokes, mouse movements, WebSocket messages, timer ticks, route changes, HTTP responses that may or may not arrive. This chapter covers the fundamentals: what an Observable is, how it differs from a Promise, the three channels of a subscription, the operators that transform streams, the distinction between cold and hot Observables, and the patterns that keep subscriptions healthy. It is the foundation for everything that follows — catchError, retry, switchMap, debounceTime, and the rest of the RxJS vocabulary that Angular code uses.

Key point: An Observable is a function that takes an observer and returns a subscription. The observer has three callbacks — next, error, and complete — and the subscription has an unsubscribe method. The Observable does nothing until subscribed; this is the “cold” behavior, and it is why HTTP requests do not fire until .subscribe() is called. Operators are functions that take an Observable and return a new Observable; they are composed with .pipe(). Subscriptions must be cleaned up, or they leak. The async pipe and the takeUntilDestroyed operator are the modern ways to do that cleanup automatically.


What an Observable is

An Observable is an object that represents a stream of values over time. It is a producer, and the consumer subscribes to receive the values. The subscription is the act of turning on the stream.

import { Observable } from 'rxjs';

const numbers$ = new Observable<number>((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
});

numbers$.subscribe({
  next: (value) => console.log(value),
  error: (err) => console.error(err),
  complete: () => console.log('done'),
});
// 1
// 2
// 3
// done

The Observable’s constructor takes a function that receives a subscriber. The function calls next with each value, then complete when the stream ends. The subscriber calls the three callbacks when it receives the notifications. The naming convention is a trailing $ for variables that hold Observables, a community standard that makes the type visible in the name.

Why the Observable does nothing until subscribed. The function passed to the constructor does not run until subscribe is called. This is what makes Observables lazy — they describe a stream, and the stream is only produced when there is a consumer. The HTTP client relies on this: this.http.get(url) returns a description, and no request is sent until subscription.

Why the three channels matter. next carries values, error carries a failure and terminates the stream, and complete signals normal termination. Only one terminal notification is delivered — either error or complete, never both. After a terminal notification, the subscription is closed and no further values are delivered. This contract is the foundation of every operator.

Why Observables can be canceled. Calling unsubscribe on a subscription stops the stream and runs any cleanup logic the Observable defined. For an HTTP request, unsubscribing cancels the request. For a timer, unsubscribing clears the interval. This is the property that makes Observables more powerful than Promises for UI work — a request that is no longer needed can be canceled.

Why the subscriber function must return cleanup. If the Observable creates a resource — a timer, a DOM listener, a WebSocket — it should return a function that cleans up. The unsubscribe method calls this function.

const interval$ = new Observable<number>((subscriber) => {
  let count = 0;
  const id = setInterval(() => subscriber.next(count++), 1000);
  return () => clearInterval(id);  // cleanup on unsubscribe
});

The returned function is called when the subscriber unsubscribes, when the stream completes, or when an error occurs. This is the mechanism that prevents leaks.

Why the lazy behavior is the source of the most common RxJS mistake. A developer coming from fetch expects http.get(url) to send a request. It does not. The Observable is a description, and the request is sent on subscription. This is why every HTTP mutation in Angular must be subscribed — the post, put, and delete methods do not fire until then. The mistake is silent: the code compiles, the call returns an Observable, and nothing happens.


Observables vs Promises

The comparison clarifies the design. Both represent asynchronous values, but the contract differs in ways that matter for every use.

AspectPromiseObservable
ValuesOneZero, one, or many
LazyEager — starts immediatelyLazy — starts on subscribe
CancelableNoYes
Operators.then, .catch.pipe with dozens
Multiple subscribersShared resultIndependent execution (cold)
ErrorsRejected onceError channel, terminal
Sync valuesNoYes

Why lazy is the important difference. A Promise starts as soon as it is created. An Observable does nothing until subscribed. This means an Observable can be created, passed around, composed, and only executed when the consumer is ready. The HTTP client uses this to allow operators like retry, delay, and switchMap to influence the request before it is sent.

Why cancelable is the other important difference. A Promise cannot be canceled — once created, it runs to completion. An Observable can be unsubscribed, which stops the work. For UI, this is essential: a search that is no longer needed should be canceled, not allowed to complete and produce a stale result.

Why multiple values matter. A Promise resolves once. An Observable can emit a sequence — every keystroke in a search box, every WebSocket message, every timer tick. The operator library is built around the assumption that a stream has many values, and the operators like debounceTime, distinctUntilChanged, and scan are designed for that.

Why the mental model shifts. With Promises, the code is await-based and sequential. With Observables, the code is pipeline-based and declarative. The operators describe what should happen to each value, and the composition describes the whole stream. The mental model is closer to functional programming than to imperative async code.


Creating Observables

RxJS provides creation functions for the common cases. Using them is almost always preferable to writing the subscriber function directly.

import { of, from, interval, timer, fromEvent, EMPTY, NEVER, throwError, defer } from 'rxjs';

of(1, 2, 3)                          // emits 1, 2, 3, then completes
from([1, 2, 3])                      // same, from an array
from(Promise.resolve(42))            // emits 42, then completes
interval(1000)                       // emits 0, 1, 2, ... every second
timer(2000)                          // emits 0 after 2 seconds, completes
fromEvent(document, 'click')         // emits a MouseEvent on every click
EMPTY                                // completes immediately, emits nothing
NEVER                                // never emits, never completes
throwError(() => new Error('x'))     // emits an error, then completes
defer(() => of(Date.now()))          // creates a new Observable per subscription

Why of and from are the workhorses. of takes a sequence of values and emits them. from takes an iterable, a Promise, or an array-like and emits its contents. These are the constructors for synchronous streams, and they are used constantly for testing and for wrapping values.

Why interval and timer are useful. They create time-based streams. interval emits repeatedly at a fixed period, timer emits once after a delay. Both are useful for polling, debouncing, and timeouts. Both are cancelable — unsubscribing clears the underlying timer.

Why fromEvent bridges DOM events. It takes a target and an event name and produces a stream of events. This is how raw DOM events can be brought into the RxJS world and composed with other streams. In Angular, most event handling uses the template binding, but fromEvent is available when a stream is needed.

Why EMPTY, NEVER, and throwError are used in operators. They are the terminal Observables. EMPTY completes without emitting, NEVER never does anything, and throwError emits an error. They are used inside catchError, switchMap, and other operators to produce the right stream for a branch.

Why defer is important for side effects. defer takes a function that returns an Observable, and calls the function for each subscription. This means the Observable is created fresh each time, which is how you make a cold Observable that captures the current state at subscription time. Without defer, the Observable is created once, and the state is captured at creation time.


Operators and pipe

An operator is a function that takes an Observable and returns a new Observable. The .pipe() method composes them left to right.

import { map, filter, take } from 'rxjs';

const result$ = of(1, 2, 3, 4, 5).pipe(
  filter((n) => n % 2 === 0),
  map((n) => n * 10),
  take(2),
);

result$.subscribe((value) => console.log(value));
// 20
// 40

The filter operator passes only even numbers, map multiplies each by 10, and take limits the stream to two values. The pipeline reads top to bottom, and each operator transforms the stream in a small, composable way.

Why pipe is the composition mechanism. The .pipe() method is a method on Observable that takes a list of operators and returns the composed result. Before pipe was introduced, operators were chained with . (like observable.map().filter()), which required patching the Observable prototype and caused problems with tree-shaking. The pipe method makes operators pure functions and keeps the prototype clean.

Why operators are pure functions. Each operator takes an Observable and returns a new Observable without modifying the original. This means an Observable can be piped in multiple ways without affecting other subscribers, and it makes the operators testable in isolation.

The categories of operators. RxJS operators fall into several groups: creation, transformation (map, scan), filtering (filter, take, debounceTime), combination (merge, concat, combineLatest), error handling (catchError, retry), multicasting (share, shareReplay), and utility (tap, finalize, delay). The full list is large, but the commonly used operators cover most cases.

Why tap is important for debugging. The tap operator performs a side effect for each value without changing the stream. It is the right tool for logging, for debugging, and for triggering external state updates that are not part of the stream’s transformation.

of(1, 2, 3).pipe(
  tap((n) => console.log('before', n)),
  map((n) => n * 2),
  tap((n) => console.log('after', n)),
).subscribe();

The two tap calls log the values before and after the map, which is how a pipeline is inspected without changing it.


Cold vs hot Observables

The distinction between cold and hot Observables explains a class of behavior that is otherwise confusing.

A cold Observable creates a new producer for each subscription. Each subscriber gets its own independent stream. of, from, interval, and HttpClient.get are cold. Subscribing twice to an HTTP Observable sends two requests.

A hot Observable shares a single producer among all subscribers. The producer runs regardless of subscriptions, and subscribers receive values from the point they subscribe. DOM events, WebSocket connections, and Subjects are hot.

Why the distinction matters. For a cold Observable, every subscriber causes the work to be done — two subscribers to an HTTP call produce two requests. For a hot Observable, the work is done once and subscribers share it. The distinction determines whether a stream is duplicated or shared.

Why HTTP is cold and this is usually right. Each HTTP call should be its own request. If the request is shared, two components that both need the data would receive the same response, which is usually fine but not always. When sharing is wanted, shareReplay converts a cold Observable into a shared one.

const users$ = this.http.get<User[]>('/api/users').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);

The shareReplay operator caches the latest value and shares it with all subscribers. The first subscription sends the request, and subsequent subscriptions receive the cached value without a new request. The refCount: true option makes the Observable reset when all subscribers unsubscribe, so a later subscription sends a fresh request.

Why hot Observables need subjects. A Subject is both an Observable and an Observer — it can be subscribed to and it can emit values. It is hot: values emitted before any subscriber is present are lost. BehaviorSubject replays the latest value to new subscribers, which is why it is used for state.

Why the distinction is a common source of bugs. A cold Observable subscribed twice produces two streams, which is sometimes the intent and sometimes a mistake. A hot Observable subscribed late misses earlier values, which is sometimes expected and sometimes surprising. Knowing which kind an Observable is determines whether sharing is needed.

Why shareReplay is often the right fix for duplicated HTTP calls. Two components subscribing to the same getUsers() call would send two requests without sharing. shareReplay makes the second subscriber receive the first request’s result, without a second request. The operator is the standard way to cache and share a cold Observable.


Subscriptions and cleanup

Every subscription must be unsubscribed, or it leaks. In a long-lived application, an unsubscribed stream continues to produce values, and the callbacks hold references to the component’s state, which prevents garbage collection.

Manual unsubscribe. The traditional pattern is to store the subscription and unsubscribe in ngOnDestroy.

export class MyComponent implements OnDestroy {
  private readonly subscription = this.service.getData().subscribe();
  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

This works for a single subscription. For multiple, an array or a Subscription that contains others is used. The pattern is verbose, and it is easy to forget one.

The async pipe. In templates, the async pipe subscribes to an Observable, renders its value, and unsubscribes when the component is destroyed.

@if (users$ | async; as users) {
  @for (user of users; track user.id) {
    <div>{{ user.name }}</div>
  }
}

The pipe is the recommended way to consume an Observable in a template because it handles subscription and cleanup automatically. It is not available for logic that must run in the component class.

The takeUntilDestroyed operator. Angular 16 added takeUntilDestroyed, which completes an Observable when the component is destroyed. It is used inside an injection context or with a DestroyRef.

export class MyComponent {
  private readonly destroyRef = inject(DestroyRef);

  constructor() {
    this.service.getData().pipe(
      takeUntilDestroyed(this.destroyRef),
    ).subscribe((data) => this.data = data);
  }
}

The operator completes the stream when the component is destroyed, which causes the subscription to clean up automatically. This is the modern replacement for the Subject + takeUntil pattern that was common before Angular 16.

Why takeUntilDestroyed is the preferred pattern. It removes the boilerplate of storing subscriptions and unsubscribing, and it ties the subscription’s lifetime to the component’s lifetime. The operator is used once per subscription, and the cleanup is automatic. The only requirement is that the component has a DestroyRef, which is available in any injectable.

Why firstValueFrom is useful for one-shot values. When only the first value of an Observable is needed, firstValueFrom converts it to a Promise that resolves with the first value.

const user = await firstValueFrom(this.http.get<User>('/api/user'));

The function is useful when the surrounding code is async/await-based and the Observable is a one-shot. The Observable is subscribed, the first value is taken, and the subscription is closed. This is the bridge between the RxJS world and the Promise world.

Why manual unsubscribe is still necessary sometimes. The async pipe and takeUntilDestroyed cover most cases, but not all. A subscription that lives longer than a component — in a service, for example — needs explicit management. The general rule is: subscribe only where you can clean up, and prefer the operators and pipes that handle it for you.


Complete Example Session

import { Component, DestroyRef, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of, from, interval, fromEvent, Subject, Subscription } from 'rxjs';
import {
  map, filter, take, takeUntil, tap, debounceTime, distinctUntilChanged,
  switchMap, catchError, shareReplay, takeUntilDestroyed, finalize,
} from 'rxjs/operators';

// ============================================
// PART 1: CREATING OBSERVABLES
// ============================================

const values$ = of(1, 2, 3);
const fromArray$ = from([10, 20, 30]);
const ticks$ = interval(1000);
const click$ = fromEvent<MouseEvent>(document, 'click');

// ============================================
// PART 2: SUBSCRIBING
// ============================================

const subscription = values$.subscribe({
  next: (v) => console.log('next:', v),
  error: (e) => console.error('error:', e),
  complete: () => console.log('complete'),
});

subscription.unsubscribe();

// ============================================
// PART 3: OPERATORS
// ============================================

of(1, 2, 3, 4, 5, 6).pipe(
  filter((n) => n % 2 === 0),
  map((n) => n * 10),
  take(2),
).subscribe((v) => console.log(v));
// 20
// 40

// ============================================
// PART 4: TAP FOR DEBUGGING
// ============================================

of(1, 2, 3).pipe(
  tap((n) => console.log('before', n)),
  map((n) => n * 2),
  tap((n) => console.log('after', n)),
).subscribe();

// ============================================
// PART 5: DEBOUNCE AND DISTINCT
// ============================================

const search$ = new Subject<string>();

search$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap((term) => this.http.get(`/api/search?q=${term}`)),
).subscribe((results) => console.log(results));

// Each keystroke resets the debounce timer.
// Only the last term after 300ms of silence is used.
// switchMap cancels the previous request.

// ============================================
// PART 6: SHARING WITH SHAREREPLAY
// ============================================

const users$ = this.http.get<User[]>('/api/users').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);

// First subscriber sends the request.
// Subsequent subscribers receive the cached value.
// When all unsubscribe, the cache resets.

// ============================================
// PART 7: CLEANUP WITH takeUntilDestroyed
// ============================================

@Component({ /* ... */ })
export class UserComponent {
  private readonly http = inject(HttpClient);
  private readonly destroyRef = inject(DestroyRef);

  readonly users = signal<User[]>([]);

  constructor() {
    this.http.get<User[]>('/api/users').pipe(
      takeUntilDestroyed(this.destroyRef),
    ).subscribe((users) => this.users.set(users));
  }
}

// The subscription is cleaned up when the component is destroyed.

// ============================================
// PART 8: ERROR HANDLING
// ============================================

this.http.get<User>('/api/user/1').pipe(
  catchError((error) => {
    console.error(error);
    return of(null);
  }),
).subscribe((user) => console.log(user));

// ============================================
// PART 9: FINALIZE
// ============================================

this.http.get<User[]>('/api/users').pipe(
  finalize(() => console.log('request finished')),
).subscribe();

// finalize runs on complete, error, or unsubscribe.

// ============================================
// PART 10: COLD VS HOT
// ============================================

// COLD — each subscription produces a new stream
const cold$ = of(1, 2, 3);
cold$.subscribe((v) => console.log('A', v));  // 1 2 3
cold$.subscribe((v) => console.log('B', v));  // 1 2 3

// HOT — values shared among subscribers
const hot$ = new Subject<number>();
hot$.subscribe((v) => console.log('A', v));
hot$.subscribe((v) => console.log('B', v));
hot$.next(1);  // both A and B receive 1

The ten parts cover creation, subscription, operators, debugging, debouncing, sharing, cleanup, error handling, finalization, and the cold/hot distinction.


Quick Reference

Creation Functions

FunctionProduces
of(...values)Each value in sequence
from(iterable)Contents of an iterable
from(Promise)The resolved value
interval(ms)0, 1, 2, … every ms
timer(ms)0 after ms, completes
fromEvent(target, name)Events
EMPTYNothing, completes
NEVERNothing, never
throwError(fn)Error, completes
defer(fn)Fresh Observable per subscribe

Common Operators

OperatorPurpose
mapTransform each value
filterKeep values matching a predicate
take(n)Take first n values
takeUntil(notifier)Take until notifier emits
tapSide effect without transformation
scanAccumulate values
debounceTime(ms)Wait for silence
distinctUntilChangedSkip consecutive duplicates
switchMapCancel previous, map to new
mergeMapMap to new, run in parallel
concatMapMap to new, run sequentially
catchErrorHandle errors
retryResubscribe on error
shareReplayShare and cache
finalizeRun on terminate

Subscription Management

TechniqueWhen
async pipeTemplates
takeUntilDestroyedComponent class
Manual unsubscribeLong-lived services
firstValueFromOne-shot async
takeUntil(subject)Pre-Angular 16 pattern

Cold vs Hot

AspectColdHot
ProducerPer subscriberShared
Exampleof, HTTPDOM events, Subject
DuplicationEach subscriberShared
SharingshareReplayNative

The Three Channels

ChannelPurpose
nextValue
errorFailure, terminal
completeSuccess, terminal

Best Practices

Do This:

// Use the async pipe in templates
@if (users$ | async; as users) { ... }                        // ✅

// Use takeUntilDestroyed in the component class
this.http.get('/api').pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); // ✅

// Use operators for transformation
.pipe(filter(pred), map(transform), take(5))                   // ✅

// Use tap for debugging
.pipe(tap((v) => console.log(v)))                              // ✅

// Share cold HTTP Observables when needed
this.http.get('/api/users').pipe(shareReplay({ bufferSize: 1, refCount: true })) // ✅

// Use switchMap for cancellable operations
search$.pipe(switchMap((q) => this.http.get(`/search?q=${q}`))) // ✅

// Use firstValueFrom for one-shot async
const user = await firstValueFrom(this.http.get<User>('/api/user')); // ✅

Don’t Do This:

// Don't subscribe without cleanup
this.http.get('/api').subscribe();  // leaks in a component   // ⚠️

// Don't nest subscriptions
this.http.get('/a').subscribe((a) => {
  this.http.get('/b').subscribe((b) => { });  // use switchMap   // ⚠️
});

// Don't use switchMap for mutation order
save$.pipe(switchMap((v) => this.http.post('/save', v)));      // ⚠️ use concatMap

// Don't share an HTTP Observable without refCount
shareReplay(1);  // never resets                                // ⚠️

// Don't ignore the error channel
.subscribe((v) => console.log(v));  // errors are silent        // ⚠️

// Don't create Observables manually when a creation function exists
new Observable((s) => { s.next(1); s.next(2); s.complete(); }); // ⚠️ use of

Common Pitfalls

PitfallProblemSolution
Forgetting to subscribeStream never runsSubscribe or use async
Not unsubscribingMemory leaktakeUntilDestroyed
Nested subscribesHard to read, leaksswitchMap
switchMap for mutationsCancels writesconcatMap
shareReplay without refCountCache never resetsrefCount: true
Cold Observable subscribed twiceDuplicated requestsshareReplay
Hot Observable subscribed lateMissed valuesBehaviorSubject
tap with side effects that should transformWrong operatorUse map
Errors not handledSilent failurescatchError
take(0)Completes immediatelyUse take(1)

Real-World Examples

1. HTTP with cleanup

this.http.get('/api/users').pipe(takeUntilDestroyed(this.destroyRef)).subscribe();

2. Search with debounce

search$.pipe(debounceTime(300), distinctUntilChanged(), switchMap(...))

3. Polling with interval

interval(5000).pipe(switchMap(() => this.http.get('/api/status'))).subscribe()

4. Shared HTTP call

this.http.get('/api/users').pipe(shareReplay({ bufferSize: 1, refCount: true }))

5. Error recovery

catchError(() => of([]))

6. Retry with backoff

retry({ count: 3, delay: (i) => i * 1000 })

7. Loading state

finalize(() => this.loading.set(false))

8. Combine two streams

combineLatest([a$, b$]).pipe(map(([a, b]) => ({ a, b })))

9. One-shot async

const value = await firstValueFrom(source$)

10. Cold to shared

const shared$ = cold$.pipe(shareReplay(1))

Visual: The Three Channels

┌──────────────────────────────────────────────────────────┐
│  Observable                                              │
│    │                                                     │
│    ├── next(value)  ──►  subscriber.next(value)          │
│    │                                                     │
│    ├── next(value)  ──►  subscriber.next(value)          │
│    │                                                     │
│    ├── error(err)   ──►  subscriber.error(err)   TERMINAL│
│    │                                                     │
│    │  OR                                                  │
│    │                                                     │
│    └── complete()   ──►  subscriber.complete()   TERMINAL│
│                                                          │
│  Only one terminal notification is delivered.            │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Cold vs Hot

┌──────────────────────────────────────────────────────────┐
│  COLD                                                    │
│                                                          │
│  cold$ = of(1, 2, 3)                                     │
│                                                          │
│  subscriber A ──► new producer ──► 1, 2, 3               │
│  subscriber B ──► new producer ──► 1, 2, 3               │
│                                                          │
│  Each subscription creates its own execution.            │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  HOT                                                     │
│                                                          │
│  hot$ = new Subject()                                    │
│                                                          │
│  producer ──► 1, 2, 3                                    │
│                  │                                       │
│                  ├──► subscriber A                       │
│                  └──► subscriber B                       │
│                                                          │
│  Both subscribers receive the same values.               │
│  Values emitted before subscription are lost.            │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Operator Pipeline

┌──────────────────────────────────────────────────────────┐
│  source                                                  │
│    │                                                     │
│    ▼                                                     │
│  filter(n => n % 2 === 0)                                │
│    │                                                     │
│    ▼                                                     │
│  map(n => n * 10)                                        │
│    │                                                     │
│    ▼                                                     │
│  take(2)                                                 │
│    │                                                     │
│    ▼                                                     │
│  subscriber                                              │
│                                                          │
│  of(1,2,3,4,5) → filter → 2,4 → map → 20,40 → take(2)    │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Subscription Cleanup

┌──────────────────────────────────────────────────────────┐
│  WITHOUT CLEANUP                                         │
│                                                          │
│  Component created                                       │
│    └── subscribe()                                       │
│           │                                              │
│           └── holds reference to component               │
│                                                          │
│  Component destroyed                                     │
│    └── subscription still active                         │
│           │                                              │
│           └── component cannot be garbage collected      │
│                                                          │
│  LEAK                                                    │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  WITH takeUntilDestroyed                                 │
│                                                          │
│  Component created                                       │
│    └── subscribe() with takeUntilDestroyed               │
│                                                          │
│  Component destroyed                                     │
│    └── DestroyRef emits                                  │
│           │                                              │
│           └── takeUntilDestroyed completes the stream    │
│                 │                                        │
│                 └── subscription cleaned up              │
│                                                          │
│  NO LEAK                                                 │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: switchMap vs concatMap

┌──────────────────────────────────────────────────────────┐
│  switchMap (cancels previous)                            │
│                                                          │
│  source:  ──a────b──────c────►                           │
│                                                          │
│  a ──► request A                                         │
│  b ──► A canceled, request B                             │
│  c ──► B canceled, request C                             │
│                                                          │
│  Only C completes.                                       │
│  Use for: search, route params, cancellable reads.       │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  concatMap (queues)                                      │
│                                                          │
│  source:  ──a────b──────c────►                           │
│                                                          │
│  a ──► request A                                         │
│  b ──► queued                                            │
│  c ──► queued                                            │
│                                                          │
│  A completes, then B, then C.                            │
│  Use for: saves, mutations, ordered writes.              │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
ObservableA stream of values over time
Three channelsnext, error, complete
LazyRuns on subscribe
Cancelableunsubscribe
OperatorsComposed with .pipe()
ColdNew producer per subscriber
HotShared producer
SharingshareReplay
Template consumptionasync pipe
Component cleanuptakeUntilDestroyed
One-shot asyncfirstValueFrom

Key takeaways:

  • An Observable is a stream — it can produce zero, one, or many values over time, unlike a Promise which produces exactly one
  • Nothing happens until subscribe — the Observable is a description, and the producer runs only when a subscriber is present
  • The three channels are the contractnext for values, error and complete for termination, and only one terminal notification is delivered
  • Operators compose with pipe — each operator takes an Observable and returns a new one, and the pipeline reads left to right
  • Cold Observables create a producer per subscriber — subscribing twice to an HTTP Observable sends two requests unless shareReplay is used
  • Hot Observables share a producer — DOM events and Subjects are hot, and values emitted before subscription are lost
  • Subscriptions must be cleaned up — the async pipe in templates and takeUntilDestroyed in component classes are the modern mechanisms
  • switchMap is for cancellable operations — search, route parameters, and reads where only the latest matters; concatMap is for ordered writes
  • firstValueFrom bridges to async — when the surrounding code is async/await and the Observable is one-shot
  • RxJS is the foundation — HTTP, forms, routing, and signals in Angular all have Observable characteristics underneath, and understanding the fundamentals makes every one of them clearer

Remember: RxJS is the reactive layer underneath Angular. The Observable is a stream, the subscription turns it on, the operators transform it, and the cleanup turns it off. Every asynchronous operation in an Angular application is either an Observable or can be modeled as one. Understanding the fundamentals — the three channels, the operator pipeline, the cold/hot distinction, and the cleanup patterns — is what makes the rest of RxJS usable rather than mysterious.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!