RxJS is not just a library of operators. It is a different way of thinking about asynchronous data.
If you work with Angular, Node.js, React, or any application that deals with asynchronous operations, you will eventually encounter problems involving:
- HTTP requests
- User input
- WebSockets
- Events
- Timers
- State changes
- Multiple asynchronous operations
- Cancellation
- Race conditions
- Retries
- Error handling
RxJS provides a powerful model for solving these problems through Reactive Programming.
In this article, we will move from the theory to practical examples and finally build a realistic Angular-style application using RxJS.
1. What Is RxJS?
RxJS (Reactive Extensions for JavaScript) is a library for composing asynchronous and event-based programs using Observables.
Instead of thinking:
"I have a function that will eventually return a value."
You start thinking:
"I have a stream of values that may arrive over time."
For example:
const observable$ = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
});
The observable represents a stream:
1 ───> 2 ───> 3 ───> complete
But an Observable doesn't have to emit only three values.
It could represent:
User clicks
↓
click
↓
click
↓
click
↓
...
Or:
HTTP Request
↓
Response
↓
Complete
Or:
WebSocket
↓
Message
↓
Message
↓
Message
↓
Message
↓
...
This is the fundamental idea behind RxJS.
2. What Is Reactive Programming?
Reactive programming is a programming paradigm based on:
Reacting to changes and events over time.
Traditional imperative programming often looks like:
const value = getValue();
console.log(value);
You ask for a value and receive it.
Reactive programming looks more like:
value$.subscribe(value => {
console.log(value);
});
You are saying:
"Whenever a value arrives, execute this logic."
This becomes extremely powerful when dealing with asynchronous systems.
3. The Core RxJS Concepts
RxJS revolves around several important concepts:
Observable
↓
Subscribe
↓
Receive values
↓
Operators
↓
Transform / Filter / Combine
↓
Observer
The most important concepts are:
- Observable
- Observer
- Subscriber
- Subscription
- Operators
- Subject
- Schedulers
Let's understand them.
4. Observable
An Observable represents a stream of values over time.
Example:
import { Observable } from 'rxjs';
const observable$ = new Observable<number>(subscriber => {
subscriber.next(10);
subscriber.next(20);
subscriber.next(30);
subscriber.complete();
});
Subscribe:
observable$.subscribe(value => {
console.log(value);
});
Output:
10
20
30
The $ suffix is a common naming convention:
users$
products$
orders$
searchResults$
It usually means:
"This variable represents an Observable."
It is a convention, not a language requirement.
5. Observable Lifecycle
An Observable can emit three kinds of notifications:
next
error
complete
Example:
const observable$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete();
});
Conceptually:
next(1)
↓
next(2)
↓
next(3)
↓
complete()
An Observable can also fail:
const observable$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.error(
new Error('Something went wrong')
);
});
Then:
next(1)
↓
error
Once an Observable sends either:
complete()
or:
error(...)
the stream terminates.
6. Observer
An Observer defines what should happen when the Observable emits values, errors, or completes.
observable$.subscribe({
next: value => {
console.log('Value:', value);
},
error: error => {
console.error('Error:', error);
},
complete: () => {
console.log('Completed');
}
});
This is more explicit than:
observable$.subscribe(value => {
console.log(value);
});
For production code, the object form is often easier to maintain when error or completion handling matters.
7. Subscription
When you subscribe:
const subscription = observable$.subscribe({
next: value => console.log(value)
});
you receive a Subscription.
You can unsubscribe:
subscription.unsubscribe();
This matters for long-lived streams such as:
- WebSockets
- DOM events
- intervals
- Subjects
- application-wide streams
Example:
const subscription = interval(1000).subscribe(value => {
console.log(value);
});
setTimeout(() => {
subscription.unsubscribe();
}, 5000);
The interval stops after approximately five seconds.
8. Cold Observables
A cold Observable starts its producer separately for each subscriber.
Example:
const observable$ = new Observable<number>(subscriber => {
console.log('Producer started');
subscriber.next(Math.random());
});
Subscribe twice:
observable$.subscribe(value => {
console.log('Subscriber 1:', value);
});
observable$.subscribe(value => {
console.log('Subscriber 2:', value);
});
The producer runs twice.
Conceptually:
Subscriber 1
↓
Producer A
Subscriber 2
↓
Producer B
Each subscriber gets its own execution.
9. Hot Observables
A hot Observable represents a shared source.
For example:
WebSocket
↓
├── Subscriber A
├── Subscriber B
└── Subscriber C
The source exists independently of individual subscribers.
This concept becomes especially important when using:
Subject
share()
shareReplay()
10. Operators
Operators are one of the most important parts of RxJS.
They allow you to transform and control streams.
For example:
source$
.pipe(
map(value => value * 2),
filter(value => value > 10)
)
.subscribe(value => {
console.log(value);
});
Think of an RxJS pipeline as:
Source
↓
map
↓
filter
↓
subscribe
11. map
map transforms every emitted value.
of(1, 2, 3)
.pipe(
map(value => value * 10)
)
.subscribe(console.log);
Output:
10
20
30
Conceptually:
1 → map → 10
2 → map → 20
3 → map → 30
A very common example is extracting data from an HTTP response:
this.http.get<ApiResponse<User[]>>('/api/users')
.pipe(
map(response => response.data)
);
Now the consumer receives only:
User[]
instead of the complete response object.
12. filter
filter allows only values matching a condition.
of(1, 2, 3, 4, 5)
.pipe(
filter(value => value % 2 === 0)
)
.subscribe(console.log);
Output:
2
4
Think:
1 ❌
2 ✅
3 ❌
4 ✅
5 ❌
13. tap
tap allows you to perform side effects without changing the emitted value.
users$
.pipe(
tap(users => {
console.log('Users:', users);
})
)
.subscribe();
tap is useful for:
- Logging
- Debugging
- Analytics
- Updating external state
- Observing the pipeline
Avoid using tap to secretly transform data.
Bad:
tap(user => {
user.name = 'Changed';
})
Prefer transformations through operators such as:
map(user => ({
...user,
name: 'Changed'
}))
14. debounceTime
One of the most practical RxJS operators.
Imagine a search box.
Without debouncing:
c
ca
cat
cats
You might send four HTTP requests.
With:
debounceTime(300)
the application waits until the user stops typing for 300ms.
searchTerm$
.pipe(
debounceTime(300)
)
Conceptually:
c
ca
cat
cats
↓ 300ms
"cats"
Only "cats" continues through the pipeline.
15. distinctUntilChanged
This operator prevents consecutive duplicate values.
of(
'Angular',
'Angular',
'React',
'React',
'Vue'
)
.pipe(
distinctUntilChanged()
)
.subscribe(console.log);
Output:
Angular
React
Vue
Very useful for:
Search inputs
Filters
Route parameters
State changes
16. switchMap
switchMap is one of the most important RxJS operators.
Imagine:
User searches:
Angular
Angular RxJS
Angular RxJS operators
Each search creates an HTTP request.
You usually don't want an old request to overwrite the latest result.
switchMap solves this by switching to the newest inner Observable and unsubscribing from the previous one.
searchTerm$
.pipe(
switchMap(term => this.searchApi(term))
)
Conceptually:
Search A
↓
Request A
Search B
↓
Cancel/Unsubscribe A
↓
Request B
Search C
↓
Cancel/Unsubscribe B
↓
Request C
This makes switchMap ideal for:
- Search
- Autocomplete
- Route parameter changes
- Refreshing data
- User-driven queries
17. mergeMap
mergeMap subscribes to inner Observables concurrently.
ids$
.pipe(
mergeMap(id => this.getUser(id))
);
Conceptually:
ID 1 → Request 1
ID 2 → Request 2
ID 3 → Request 3
All can run concurrently.
Use it when previous operations should not be cancelled.
Typical use cases:
- Independent HTTP requests
- Processing multiple items
- Concurrent operations
18. concatMap
concatMap queues inner Observables and runs them sequentially.
actions$
.pipe(
concatMap(action => this.save(action))
);
Conceptually:
Action A
↓
Request A
↓
Complete
Action B
↓
Request B
↓
Complete
Action C
↓
Request C
Use it when order matters.
For example:
Create record A
↓
Update A
↓
Delete A
You may not want these operations running concurrently.
19. exhaustMap
exhaustMap ignores new emissions while the current inner Observable is running.
Example:
submitClick$
.pipe(
exhaustMap(() => this.submitForm())
);
Imagine the user double-clicks:
Click
↓
Request starts
Click
↓
IGNORED
Click
↓
IGNORED
Request completes
This is extremely useful for preventing duplicate submissions.
20. The Four Important Mapping Operators
A useful mental model:
| Operator | Behavior |
|---|---|
switchMap |
Cancel previous |
mergeMap |
Run concurrently |
concatMap |
Queue sequentially |
exhaustMap |
Ignore while busy |
Think about them like this:
switchMap
Latest wins
mergeMap
Everything runs
concatMap
One by one
exhaustMap
First wins while busy
Choosing the correct flattening operator is one of the most important RxJS skills.
21. catchError
Errors are inevitable.
RxJS provides:
catchError()
Example:
this.http.get<User[]>('/api/users')
.pipe(
catchError(error => {
console.error(error);
return of([]);
})
);
Instead of terminating the application flow unexpectedly, the stream returns an empty array.
However, error handling should reflect the application's requirements.
Sometimes you should recover:
catchError(() => of([]))
Sometimes you should rethrow:
catchError(error => {
return throwError(() => error);
})
22. retry
Transient failures can sometimes be retried.
this.http.get('/api/data')
.pipe(
retry(3)
);
Conceptually:
Request
↓
Fail
↓
Retry
↓
Fail
↓
Retry
↓
Fail
↓
Retry
But blindly retrying everything is a bad idea.
For example, retrying a validation error won't usually solve anything.
Retries are more appropriate for transient failures.
23. finalize
finalize runs when the Observable terminates because of completion, error, or unsubscription.
Example:
this.loading = true;
this.http.get('/api/users')
.pipe(
finalize(() => {
this.loading = false;
})
)
.subscribe();
This is useful for cleanup:
Start request
↓
loading = true
↓
HTTP request
↓
complete/error/unsubscribe
↓
loading = false
24. Creating Observables
RxJS provides many creation functions.
of
of(1, 2, 3)
Emits:
1
2
3
from
from([1, 2, 3])
Also emits:
1
2
3
But from can convert many iterable or Promise-like sources.
For example:
from(fetch('/api/users'))
25. interval
Creates periodic emissions.
interval(1000)
.subscribe(value => {
console.log(value);
});
Output:
0
1
2
3
4
...
This Observable does not naturally complete.
Therefore, long-lived subscriptions should be managed carefully.
26. timer
timer(2000)
Emits after two seconds.
You can also create repeated emissions:
timer(0, 1000)
Conceptually:
0
↓ 1 sec
1
↓ 1 sec
2
↓ 1 sec
3
...
27. Subjects
A Subject is both:
- an Observable
- an Observer
Example:
const subject$ = new Subject<number>();
subject$.subscribe(value => {
console.log('A:', value);
});
subject$.subscribe(value => {
console.log('B:', value);
});
subject$.next(10);
subject$.next(20);
Output:
A: 10
B: 10
A: 20
B: 20
The Subject multicasts values to its subscribers.
28. BehaviorSubject
BehaviorSubject requires an initial value and stores the latest value.
const user$ = new BehaviorSubject<User | null>(null);
When a new subscriber arrives, it immediately receives the current value.
Current value = User A
Subscriber joins
↓
Receives User A
This makes BehaviorSubject useful for representing state.
Example:
private currentUserSubject =
new BehaviorSubject<User | null>(null);
currentUser$ =
this.currentUserSubject.asObservable();
Then:
this.currentUserSubject.next(user);
29. ReplaySubject
ReplaySubject can replay previous emissions.
const subject$ = new ReplaySubject<number>(2);
subject$.next(1);
subject$.next(2);
subject$.next(3);
subject$.subscribe(value => {
console.log(value);
});
Output:
2
3
because we configured it to replay the last two values.
30. combineLatest
Sometimes you need the latest value from multiple streams.
Example:
combineLatest([
user$,
settings$,
permissions$
])
Conceptually:
User ────────┐
│
Settings ────┼──> combineLatest
│
Permissions ─┘
It emits when one source emits, after every source has emitted at least once.
Useful for:
Filters
UI state
User preferences
Multiple dependent inputs
31. forkJoin
forkJoin waits for all supplied Observables to complete and then emits their final values.
Perfect for independent HTTP requests:
forkJoin({
users: this.getUsers(),
products: this.getProducts(),
orders: this.getOrders()
})
Conceptually:
Users ────────┐
Products ─────┼──> forkJoin → result
Orders ───────┘
Important distinction:
forkJoinis generally for "wait until all complete."
combineLatestis generally for "react whenever the latest values change."
32. withLatestFrom
Sometimes one Observable should trigger the operation while reading the latest value from another.
Example:
submit$
.pipe(
withLatestFrom(formValue$)
)
Think:
submit$ ────────────────┐
↓
withLatestFrom
↑
formValue$ ─────────────┘
The submit event is the trigger.
33. RxJS and Angular
RxJS is deeply integrated into Angular.
Common examples include:
HttpClient
ActivatedRoute
Router events
Reactive Forms
Event streams
NgRx
WebSockets
For example:
this.route.params
.pipe(
switchMap(params =>
this.userService.getUser(params['id'])
)
);
This is a classic Angular pattern.
34. Real-World Search Example
Let's build a search pipeline.
We have:
searchControl = new FormControl('');
Then:
searchResults$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(term => !!term && term.length >= 2),
switchMap(term =>
this.searchService.search(term)
)
);
Let's understand the pipeline:
User types
↓
valueChanges
↓
debounceTime
↓
distinctUntilChanged
↓
filter
↓
switchMap
↓
HTTP request
↓
results
This is a real RxJS use case.
35. Why switchMap Matters Here
Imagine:
User types: angular
Request A starts
Then:
User types: angular rxjs
Request B starts
The old search is no longer relevant.
With:
switchMap()
the pipeline switches to the newest request.
Without the correct concurrency strategy, an older response could potentially create stale UI state depending on how the requests are managed.
36. Preventing Duplicate Form Submissions
Consider:
submit$
.pipe(
exhaustMap(() => this.api.submitForm())
)
.subscribe();
If the user clicks:
Submit
Submit
Submit
while the first request is running:
Submit #1 → Request
Submit #2 → ignored
Submit #3 → ignored
This is one of the clearest real-world uses for exhaustMap.
37. Sequential API Operations
Suppose we need:
Create Order
↓
Create Payment
↓
Send Confirmation
You may need sequential execution.
For a stream of operations:
operations$
.pipe(
concatMap(operation =>
this.execute(operation)
)
);
concatMap preserves order by waiting for the previous inner Observable to complete.
38. Parallel Requests
If operations are independent:
ids$
.pipe(
mergeMap(id => this.loadUser(id))
);
Requests can execute concurrently.
If you need to limit concurrency, mergeMap also accepts a concurrency parameter:
mergeMap(
id => this.loadUser(id),
3
)
Now at most three inner subscriptions run concurrently.
39. RxJS Pipeline Thinking
One of the biggest improvements in RxJS comes from learning to think in pipelines.
Instead of:
let result = input;
result = transform(result);
if (condition) {
result = anotherTransform(result);
}
send(result);
You can model the process as:
input$
.pipe(
transform(),
filter(),
anotherTransform(),
tap(),
...
)
.subscribe();
The stream becomes a data-processing pipeline.
40. Avoid Nested Subscriptions
A common beginner pattern is:
this.userService.getUser().subscribe(user => {
this.orderService.getOrders(user.id).subscribe(orders => {
console.log(orders);
});
});
This can become difficult to maintain.
Instead:
this.userService.getUser()
.pipe(
switchMap(user =>
this.orderService.getOrders(user.id)
)
)
.subscribe(orders => {
console.log(orders);
});
This is often called:
Flattening Observables
and it is one of the main reasons higher-order mapping operators exist.
41. Higher-Order Observables
Suppose:
Observable<User>
is normal.
But:
Observable<Observable<User>>
is a higher-order Observable.
For example:
users$
.pipe(
map(user => this.getUserDetails(user.id))
);
Now the result becomes conceptually:
Observable
↓
Observable<User>
This is where flattening operators become important:
switchMap
mergeMap
concatMap
exhaustMap
They turn:
Observable<Observable<T>>
into something like:
Observable<T>
while applying different concurrency behavior.
42. shareReplay
Suppose multiple components need the same HTTP result.
Without sharing:
users$ = this.http.get<User[]>('/api/users');
Depending on how it is consumed, multiple subscriptions can trigger multiple HTTP executions.
A common caching/sharing pattern is:
users$ = this.http.get<User[]>('/api/users')
.pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
Conceptually:
┌── Component A
HTTP Request ─┼── Component B
└── Component C
The shared result can be replayed to later subscribers while the shared subscription is active.
Be deliberate with caching semantics, especially for data that can become stale.
43. Memory Leaks
Long-lived streams can cause memory leaks when subscriptions remain active unnecessarily.
Potential examples:
interval(...)
fromEvent(...)
WebSocket streams
Subjects
Modern Angular provides tools such as:
takeUntilDestroyed()
For example:
this.events$
.pipe(
takeUntilDestroyed(this.destroyRef)
)
.subscribe();
This ties the subscription lifetime to the Angular destruction lifecycle.
The important principle is:
Subscription lifetime should match the lifetime of the work you actually need.
44. take
You can limit the number of emissions:
interval(1000)
.pipe(
take(3)
)
.subscribe(console.log);
Output:
0
1
2
Then the Observable completes.
45. takeUntil
Another common pattern:
source$
.pipe(
takeUntil(destroy$)
)
.subscribe();
When:
destroy$.next();
the subscription terminates.
In modern Angular applications, Angular's destruction utilities can often provide a cleaner alternative.
46. Error Handling Strategy
A production RxJS pipeline might look like:
this.api.getUsers()
.pipe(
retry(2),
catchError(error => {
this.logger.error(error);
return of([]);
}),
finalize(() => {
this.loading = false;
})
)
.subscribe(users => {
this.users = users;
});
Notice the responsibilities:
retry
↓
Transient failure handling
catchError
↓
Recovery
finalize
↓
Cleanup
Each operator has a specific responsibility.
47. RxJS Architecture
A clean Angular application might have:
Component
↓
Facade / State Layer
↓
Service
↓
HttpClient
↓
Backend API
RxJS can flow through all these layers.
For example:
Component
↓
users$
↓
UserService
↓
HTTP Observable
↓
Backend
The component does not necessarily need to manually manage every asynchronous operation.
48. Async Pipe
Angular's async pipe can subscribe to an Observable in the template.
Example:
users$ = this.userService.getUsers();
Template:
<ul>
@for (user of users$ | async; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
The async pipe handles subscription and cleanup for the template binding.
This can reduce manual subscription management.
49. Observable vs Promise
Promises represent one eventual result:
Promise
↓
one result
Observables can represent:
Observable
↓
0
1
2
3
...
A Promise:
fetch('/api/users')
.then(response => response.json());
Observable:
this.http.get<User[]>('/api/users')
Observables also provide rich operators for:
Transformation
Filtering
Cancellation
Combination
Concurrency
Retrying
Error handling
Composition
50. Cancellation
One major difference is cancellation.
Promises do not natively provide the same compositional cancellation model as RxJS subscriptions.
With an Observable:
const subscription = source$.subscribe();
subscription.unsubscribe();
This gives RxJS a powerful way to model work that should stop when it is no longer needed.
This becomes particularly useful with:
Search
Navigation
Live streams
Polling
User interactions
51. RxJS Mental Model
A useful mental model is:
Observable
↓
Source of values
Operator
↓
Transforms or controls values
Subscription
↓
Starts/owns the execution
Observer
↓
Consumes values
Think:
SOURCE
↓
↓
map
↓
filter
↓
switchMap
↓
catchError
↓
finalize
↓
SUBSCRIBER
Once this mental model becomes natural, RxJS becomes much easier.
52. A Complete Real-World Example
Let's combine everything.
Imagine an Angular product search page.
Requirements:
- User enters a search term.
- Wait 300ms.
- Ignore duplicate terms.
- Ignore short terms.
- Cancel previous searches.
- Retry temporary failures.
- Show loading state.
- Return an empty result if the request fails.
Implementation:
searchResults$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
map(term => term?.trim() ?? ''),
filter(term => term.length >= 2),
tap(() => {
this.loading = true;
}),
switchMap(term =>
this.productService.search(term).pipe(
retry(2),
catchError(error => {
console.error('Search failed:', error);
return of([]);
}),
finalize(() => {
this.loading = false;
})
)
)
);
This pipeline represents a complete reactive workflow:
User Input
↓
debounce
↓
remove duplicates
↓
normalize
↓
validate
↓
loading = true
↓
switchMap
↓
HTTP
↓
retry
↓
catchError
↓
finalize
↓
Results
This is where RxJS becomes much more than just syntax.
53. How to Choose the Correct Operator
When you see an asynchronous problem, ask:
Do I need to transform values?
Use:
map
Do I need to remove values?
Use:
filter
Do I need to wait for user input to settle?
Use:
debounceTime
Do I need to ignore consecutive duplicates?
Use:
distinctUntilChanged
Do I need only the newest request?
Use:
switchMap
Do I need concurrent operations?
Use:
mergeMap
Do I need sequential operations?
Use:
concatMap
Do I need to ignore new triggers while busy?
Use:
exhaustMap
Do I need all requests to finish?
Use:
forkJoin
Do I need the latest values from multiple streams?
Use:
combineLatest
Do I need error recovery?
Use:
catchError
Do I need retries?
Use:
retry
Do I need cleanup?
Use:
finalize
54. The Most Important RxJS Skill
The hardest part of RxJS is not memorizing operators.
It is understanding time and concurrency.
For example:
User Event A
↓
Request A
User Event B
↓
Request B
What should happen to Request A?
Different answers produce different operators:
Cancel A → switchMap
Keep A + B → mergeMap
Wait A → B → concatMap
Ignore B → exhaustMap
Once you understand this question, choosing the operator becomes much easier.
55. Common RxJS Mistakes
Mistake 1: Nested subscriptions
Avoid:
a$.subscribe(a => {
b$.subscribe(b => {
...
});
});
Prefer composition:
a$
.pipe(
switchMap(a => b$)
)
.subscribe();
Mistake 2: Using subscribe everywhere
Don't subscribe just to pass data to another Observable.
Instead of:
this.userService.getUser().subscribe(user => {
this.user$ = of(user);
});
prefer:
this.user$ = this.userService.getUser();
Mistake 3: Choosing switchMap automatically
switchMap is not always the correct operator.
If every operation must complete:
concatMap
may be better.
If operations are independent:
mergeMap
may be better.
If duplicate clicks should be ignored:
exhaustMap
may be better.
Mistake 4: Ignoring subscription lifetime
Long-running subscriptions should have intentional lifecycle management.
Mistake 5: Overusing Subjects
Subjects are useful, but not every Observable needs to become a Subject.
Prefer simple Observable composition when possible.
56. RxJS in One Diagram
The entire philosophy can be summarized as:
RxJS
│
▼
Observable
│
▼
Stream of values
│
┌──────────┴──────────┐
▼ ▼
Operators Subscription
│ │
▼ ▼
Transform / Filter Start / Cleanup
Combine / Control │
│ │
└──────────┬──────────┘
▼
Observer
│
▼
Application Logic
57. Final Takeaways
RxJS becomes much easier when you stop thinking about it as:
"A huge library with hundreds of operators."
Instead, think of it as:
A way to model values and events over time.
The most important concepts to master are:
Observable
Observer
Subscription
Operators
Subjects
Higher-order Observables
Concurrency
Error handling
Cancellation
And the operators you should know particularly well are:
map
filter
tap
debounceTime
distinctUntilChanged
switchMap
mergeMap
concatMap
exhaustMap
catchError
retry
finalize
combineLatest
forkJoin
withLatestFrom
take
takeUntil
share
shareReplay
If you truly understand:
switchMap
mergeMap
concatMap
exhaustMap
you already understand one of the most important parts of practical RxJS.
The goal is not to memorize every operator.
The goal is to look at a problem and ask:
"How should these values behave over time?"
Once you can answer that question, the RxJS operator usually becomes obvious.
Conclusion
RxJS is powerful because asynchronous behavior is rarely just:
request → response
Real applications contain:
events
requests
cancellation
retries
multiple streams
user interactions
state changes
concurrency
errors
RxJS gives us a unified model for composing all of them.
And that's why RxJS remains such an important skill for modern Angular and reactive JavaScript development.
Don't learn RxJS by memorizing operators. Learn it by understanding streams, time, cancellation, and concurrency.

Top comments (0)