When I discovered RxJS a few years ago with Angular, I just saw .subscribe() everywhere on things called observables.
But what I didn’t see yet were memory leaks, weird behaviors, and wobbly operator chains.
Over time, I understood that reactive programming is much more than a tool for managing API calls. It’s a paradigm shift, which also implies good practices and a different mental model.
But first, what is an Observable?
An observable is an asynchronous data stream that can be listened to over time.
It’s at the heart of reactive programming with RxJS.
The basic idea: a source that emits values
Imagine a pipe through which data arrives over time:
const obs$ = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
Here, we create a stream that emits 1, 2, 3, then stops.
To “listen” to this stream, we use subscribe():
obs$.subscribe(value => console.log(value));
// outputs: 1, 2, 3
Difference with a Promise
| Feature | Promise | Observable |
|---|---|---|
| Values | Single | Multiple over time |
| Cancelable | No | Yes |
| Lazy (on demand) | Yes | Yes |
| Operators | .then(), .catch() | .pipe(), map(), filter()… |
| Execution | Upon creation | At subscribe() time |
Concrete example: a user search
searchInput$.pipe(
debounceTime(300),
switchMap(query => this.api.search(query))
).subscribe(results => {
this.results = results;
});
searchInput$is an observable of keyboard events: we get changes from the “Search for a user” fielddebounceTime(300): waits for a 300ms pause: we want to avoid launching a search every time a user presses a key. Thedebouncehere waits 300ms after the last emitted value to continue the streamswitchMap(...): we switch observables in the stream:this.api.search(query)will instantiate a new observable (an http request) that we really want to wait for to get its result. So we switch streamssubscribe(...): receives results and updates the UI (actually, if you’re doing Angular, I prefer the| asyncpipe)
An observable can:
- emit 0, 1 or multiple values
- be infinite (for example:
fromEvent,interval) - be cold or hot (see next section)
- be combined, transformed, filtered, delayed, retried, etc.
RxJS provides over 60 operators to compose streams in an expressive and readable way.
Cold vs Hot Observable: understanding the difference
Cold Observable
An observable is said to be “cold” when the source is re-evaluated for each subscription. This means each subscriber has its own lifecycle.
const cold$ = new Observable(observer => {
console.log('New subscriber');
observer.next(Math.random());
});
cold$.subscribe(val => console.log('Subscriber 1:', val));
cold$.subscribe(val => console.log('Subscriber 2:', val));
New subscriber
Subscriber 1: 0.42
New subscriber
Subscriber 2: 0.88
Typically: http.get(), of(), from()…
Hot Observable
An observable is said to be “hot” when it shares its data source between subscribers.
const subject = new Subject();
subject.subscribe(val => console.log('Subscriber 1:', val));
subject.next(1);
subject.next(2);
subject.subscribe(val => console.log('Subscriber 2:', val));
subject.next(3);
Subscriber 1: 1
Subscriber 1: 2
Subscriber 1: 3
Subscriber 2: 3
Typically: Subject, fromEvent, WebSocket, etc.
Cold + sharing = hot (with shareReplay)
It’s possible to make a cold observable “hot” by sharing it with share() or shareReplay():
const api$ = this.http.get('/data').pipe(shareReplay(1));
This avoids triggering multiple HTTP requests if multiple subscribers subscribe.
Best practices to adopt
With great power comes great responsibility, as the uncle would say.
1. Always handle unsubscription
A subscribe() without unsubscribe() = guaranteed memory leak.
By default, subscriptions to observables don’t trigger automatic unsubscription (like JS events actually).
What to do:
-
Use
takeUntil()ortake(1): the operator completes the observable automaticallytakeUntil(notifier$)
It works by listening to another observable
notifier$which, when it emits, causes completion (and thus unsubscription) of the source observable.It doesn’t trigger unsubscribe until this notifier emits, but once done, unsubscription is automatic.
take(1)automatically completes the observable after the first emission, which triggers an automatic unsubscribe for this subscription.
-
Unsubscribe
unsubscribe()in your component’sngOnDestroy -
Use
asyncpipe in Angular templates: Theasyncpipe handles everything: it subscribes AND unsubscribes automatically.
<div *ngIf="data$ | async as data">
{{ data.title }}
</div>
2. Avoid nested subscriptions
Don’t do this:
this.a$.subscribe(a => {
this.b$.subscribe(b => {
// ...
});
});
Use switchMap, mergeMap, concatMap, depending on the desired behavior:
this.result$ = this.a$.pipe(
switchMap(a => this.b$)
);
Why?
- Because you need three boxes of Aspirin to read a pyramid of doom (you know, when you have a bunch of nested
subscribe()… - Error handling becomes complicated: each
subscribe()manages its own errors. Nesting = guaranteed acrobatics to propagate or centralize errors correctly - Guaranteed memory leaks
- Uncontrolled execution order
3. Choose the right operator
There’s a whole bunch of them, so don’t hesitate to pick the most relevant one! Some I use the most:
-
switchMap(): cancels the previous observable on each new emissionUseful for real-time user searches
-
debounceTime(500): waits for a calm period before emittingUseful for reducing noise on search fields
-
map(val => val * 2): transforms valuesSimple data manipulation
-
throttleTime(2000): ignores emissions too close togetherAnti-spam on submit button
-
shareReplay(1): shares and caches the latest valuesTo avoid calling an API 5 times for 5 subscribers
Link to go further: learnrxjs.io
Think long-term:
RxJS is an extremely powerful tool, but without good practices, it can quickly become hard to manage.
Before diving into code, take time to think about:
- The “temperature” of your observables: clearly distinguish “cold” observables (which produce data on each subscription) from “hot” ones (which emit independently of subscribers).
- Who subscribes, when and why: understand the lifecycle of your subscriptions to avoid memory leaks.
- How to handle cancellation: always plan how and when your subscriptions should be cleaned up (unsubscribed).
- The robustness of your streams: imagine your operator chains capable of handling errors, interruptions, and even evolving without breaking the application (for example with logical rollbacks).
Finally, test your RxJS streams independently from components to guarantee their reliability and ease maintenance.
In summary
RxJS is not just a tool for “making API calls” or “listening to events”.
It’s a structured and declarative way of thinking about asynchronous data stream management.
And like any powerful tool misused… it can become a nightmare.
And you, what are your favorite RxJS operators?

