Angular 17+ introduced Signals, causing developers to ask how they compare to Observables—and whether they replace RxJS.
This guide explains the differences clearly and ends with practical error-handling patterns you should use in real projects.
1. What Are Observables? (Short Recap)
Observables are asynchronous data streams from RxJS.
They are ideal for:
Key characteristics:
Lazy (execute when subscribed)
Multicast/Unicast depending on usage
Can be cancelled
Powerful because of operators
2. What Are Signals? (Short Recap)
Signals in Angular are reactive variables that trigger change detection efficiently.
They are ideal for:
Key characteristics:
Example
const count = signal(0);
count.set(5);
console.log(count());
3. Observables vs Signals — When to Use What?
A) Use Observables When You Need Streams
Perfect for:
They support operators like:
map, switchMap, mergeMap
catchError, debounceTime
forkJoin, combineLatest
retry, delay, takeUntil
B) Use Signals When You Need State
Use signals for:
Signals shine for:
Simplicity
Performance
Predictable reactivity
C) Should You Replace Observables With Signals?
NO.
Observables handle asynchronous streams.
Signals handle reactive state.
They complement each other.
You can even convert between them:
toSignal(myObservable$);
toObservable(mySignal);
4. Mixing Observables and Signals (Best Practices)
Best Practice 1: Use Signals for Component State
Example
const users = signal<User[]>([]);
Best Practice 2: Convert HTTP Observables → Signals
Clean approach:
users = toSignal(this.http.get<User[]>('/api/users'));
Best Practice 3: Avoid Signals for Continuous Streams
Bad use case:
Scroll events
Mouse move
WebSockets
Use Observables.
5. Error Handling Patterns in RxJS (Practical Guide)
Error handling is where RxJS shines.
Below are the patterns you should use in real-world Angular apps.
Pattern 1: catchError for Transforming Errors
Recommended for HTTP.
this.http.get('/api/items').pipe(
catchError(err => {
console.error('API failed', err);
return of([]); // fallback
})
)
Pattern 2: retry / retryWhen for Network Retry Logic
Auto retry:
httpCall$.pipe(
retry(3)
);
Retry with delay:
httpCall$.pipe(
retryWhen(errors =>
errors.pipe(delay(2000))
)
);
Pattern 3: finalize for Cleanup
Useful for loaders/spinners.
loading.set(true);
this.http.get('/api').pipe(
finalize(() => loading.set(false))
);
Pattern 4: Centralized Error Handling with tap
Log all errors in one place.
stream$.pipe(
tap({
error: (e) => logService.log(e)
})
);
Pattern 5: takeUntil for Auto-Unsubscribe
Best for ngOnDestroy.
private destroy$ = new Subject<void>();
obs$.pipe(
takeUntil(this.destroy$)
).subscribe();
Pattern 6: Fallback Strategy Using onErrorResumeNext
onErrorResumeNext(
api1$,
api2$
);
Ensures failure doesn't break the entire chain.
6. Which Should You Use for a New Angular App?
| Scenario | Use Observables | Use Signals |
|---|
| HTTP requests | ✔️ | via toSignal() |
| UI state | ❌ | ✔️ |
| Complex async flows | ✔️ | ❌ |
| Local component state | ❌ | ✔️ |
| Real-time events | ✔️ | ❌ |
7. Summary (Easy to Remember)
Observables
Signals