Error Handling in RxJS
Learn Error Handling in RxJS with Angular including catchError, retry, retryWhen, throwError, finalize, timeout, enterprise examples, architecture diagrams, best practices, and interview questions.
Error handling is one of the most important aspects of building robust Angular applications.
Unlike synchronous code, errors in RxJS Observables travel through the Observable stream and must be handled using RxJS operators.
Proper error handling helps applications:
- Recover gracefully
- Retry temporary failures
- Display meaningful error messages
- Prevent application crashes
- Improve user experience
RxJS error handling is one of the most frequently asked Angular interview topics.
Table of Contents
- Understanding Errors in RxJS
- Observable Lifecycle
- Error Handling Architecture
- subscribe() Error Callback
- catchError()
- throwError()
- retry()
- retryWhen()
- finalize()
- timeout()
- Enterprise Banking Example
- Performance Best Practices
- Common Mistakes
- Top 10 Interview Questions
- Summary
Understanding Errors in RxJS
An Observable has only three possible outcomes:
- Emits values (
next) - Completes (
complete) - Fails (
error)
Once an Observable emits an error, it terminates and does not emit additional values.
Observable Lifecycle
flowchart LR
A["Observable Created"]
B["next(1)"]
C["next(2)"]
D["next(3)"]
E["complete()"]
F["error()"]
G["End"]
A --> B
B --> C
C --> D
D --> E
D --> F
E --> G
F --> G
Error Handling Architecture
flowchart LR
Observable
Observable --> Operators
Operators --> catchError
catchError --> Subscriber
catchError --> FallbackObservable
Handling Errors in subscribe()
The simplest approach is handling errors in the subscription.
this.http
.get<User[]>(this.api)
.subscribe({
next: users =>
console.log(users),
error: error =>
console.error(error),
complete: () =>
console.log('Completed')
});
Advantages
- Simple
- Good for small components
Limitations
- Difficult to reuse
- Error handling logic becomes duplicated
- Cannot recover inside the Observable pipeline
catchError()
catchError() intercepts an error and returns another Observable.
this.http
.get<User[]>(this.api)
.pipe(
catchError(error => {
console.error(error);
return of([]);
})
)
.subscribe();
If the request fails,
an empty array is emitted instead.
catchError Flow
sequenceDiagram
participant Observable
participant catchError
participant Subscriber
Observable->>catchError: Error
catchError->>Subscriber: Fallback Value
Returning a Default Value
products$
.pipe(
catchError(() =>
of([])
)
)
.subscribe();
Useful when an empty result is acceptable.
Rethrowing Errors
Sometimes the error should continue to propagate.
import { throwError } from 'rxjs';
this.http
.get(this.api)
.pipe(
catchError(error => {
return throwError(
() => error
);
})
)
.subscribe();
This allows higher layers, such as global interceptors or components, to decide how to handle the error.
throwError()
throwError() creates an Observable that immediately emits an error.
return throwError(
() =>
new Error(
'Invalid Customer'
)
);
Common use cases:
- Validation failures
- Business rule violations
- Custom service errors
- Testing error scenarios
throwError Architecture
flowchart LR
Validation
Validation --> throwError
throwError --> Subscriber
retry()
retry() automatically retries a failed Observable.
this.http
.get(this.api)
.pipe(
retry(3)
)
.subscribe();
The request is attempted up to three additional times before the error is passed downstream.
retry Flow
flowchart LR
Request
Request --> Failed
Failed --> Retry1
Retry1 --> Retry2
Retry2 --> Retry3
Retry3 --> Success
Retry3 --> Error
When to Use retry()
Good candidates:
- Temporary network issues
- Short-lived server problems
- Timeout errors
- Intermittent connectivity
Avoid using retry() for:
- Validation errors
- Authentication failures (401)
- Authorization failures (403)
- Permanent business rule errors
retryWhen()
retryWhen() provides custom retry behavior.
this.http
.get(this.api)
.pipe(
retryWhen(errors =>
errors.pipe(
delay(2000),
take(3)
)
)
)
.subscribe();
This example waits 2 seconds before each retry and retries up to 3 times.
retryWhen Architecture
flowchart TD
Request
Request --> Error
Error --> retryWhen
retryWhen --> Delay
Delay --> Retry
Retry --> Success
Retry --> Error
finalize()
finalize() executes cleanup logic regardless of success or failure.
this.http
.get(this.api)
.pipe(
finalize(() => {
this.loading = false;
})
)
.subscribe();
Typical uses:
- Hide loading spinner
- Release resources
- Log completion
- Stop timers
finalize Flow
flowchart LR
Observable
Observable --> Success
Observable --> Error
Success --> finalize
Error --> finalize
finalize --> Cleanup
timeout()
timeout() throws an error if no value is emitted within the specified time.
this.http
.get(this.api)
.pipe(
timeout(5000)
)
.subscribe();
If the request exceeds 5 seconds, a timeout error is generated.
timeout Architecture
flowchart LR
Request
Request --> Timer
Timer --> Timeout
Timeout --> Error
Combining Operators
this.http
.get<User[]>(this.api)
.pipe(
timeout(5000),
retry(2),
catchError(error => {
return of([]);
}),
finalize(() => {
this.loading = false;
})
)
.subscribe();
Execution order:
- Timeout check
- Retry on eligible failures
- Return fallback value if retries fail
- Always execute cleanup
Error Handling Pipeline
flowchart LR
Request
Request --> timeout
timeout --> retry
retry --> catchError
catchError --> finalize
finalize --> Subscriber
Enterprise Banking Example
Online Banking System
Error Handling Strategy
- Retry temporary network failures
- Display user-friendly messages
- Log technical details
- Hide loading indicators
- Redirect unauthorized users
- Return safe fallback values where appropriate
Architecture
flowchart TD
AngularApp
AngularApp --> HttpInterceptor
HttpInterceptor --> BankingAPI
BankingAPI --> Success
BankingAPI --> Error
Error --> Retry
Retry --> catchError
catchError --> UI
Benefits
- Improved reliability
- Better user experience
- Consistent error handling
- Easier troubleshooting
- Cleaner architecture
Global HTTP Error Handling
A common enterprise approach is to use an HTTP Interceptor.
Responsibilities include:
- Logging errors
- Adding authentication headers
- Refreshing expired tokens
- Redirecting unauthorized users
- Showing standard error notifications
Business-specific recovery can still be handled inside services using RxJS operators.
Performance Best Practices
| Practice | Benefit |
|---|---|
| Retry only transient failures | Avoid unnecessary requests |
| Return meaningful fallback values | Better user experience |
Use finalize() for cleanup |
Consistent resource management |
| Keep error handling close to business logic | Better maintainability |
| Use HTTP Interceptors for cross-cutting concerns | Centralized handling |
| Log technical details separately from user messages | Better security and diagnostics |
Common Mistakes
Using retry() for Every Error
Avoid retrying:
- Invalid credentials
- Validation failures
- Authorization errors
Retry only transient failures.
Swallowing Errors
Bad
catchError(() =>
EMPTY
)
If no fallback or notification is provided, users may not realize an operation failed.
Forgetting finalize()
this.loading = true;
Without finalize(), loading indicators may never stop when an error occurs.
Handling Every Error in subscribe()
Avoid duplicating error handling across components.
Prefer reusable service methods and HTTP Interceptors for shared logic.
Returning the Wrong Type
Ensure the Observable returned by catchError() matches the expected stream type.
Correct
catchError(() =>
of([])
)
for an Observable<User[]>.
Best Practices
- Use
catchError()for recovery. - Use
retry()only for transient failures. - Use
retryWhen()for custom retry strategies. - Use
throwError()to propagate errors. - Use
finalize()for cleanup. - Use
timeout()for slow operations. - Keep reusable logic inside services or interceptors.
- Show friendly messages to users.
- Log technical details securely.
- Write unit tests for error scenarios.
Advantages
| Operator | Benefit |
|---|---|
catchError() |
Recover gracefully |
throwError() |
Emit custom errors |
retry() |
Automatic retries |
retryWhen() |
Custom retry strategy |
finalize() |
Guaranteed cleanup |
timeout() |
Detect slow requests |
Top 10 Interview Questions
1. What happens when an RxJS Observable emits an error?
Answer
The Observable terminates immediately, emits the error to subscribers, and does not produce additional values.
2. What is catchError()?
Answer
catchError() intercepts an error and replaces the failed Observable with another Observable, allowing recovery or fallback behavior.
3. What is throwError()?
Answer
throwError() creates an Observable that immediately emits an error. It is commonly used for custom validation or rethrowing errors.
4. When should you use retry()?
Answer
Use retry() for temporary failures such as intermittent network issues or transient server errors, but avoid using it for permanent business or authentication errors.
5. What is retryWhen()?
Answer
retryWhen() enables custom retry strategies such as delayed retries, exponential backoff, or conditional retry logic.
6. What does finalize() do?
Answer
finalize() executes cleanup logic when an Observable completes or errors.
7. What is timeout()?
Answer
timeout() throws an error if the Observable does not emit within a specified time.
8. Where should global HTTP error handling be implemented?
Answer
Global concerns such as logging, authentication, and common error responses are typically handled using Angular HTTP Interceptors.
9. Should every error be retried?
Answer
No. Only transient failures should be retried. Validation and authorization errors generally require user action rather than automatic retries.
10. What are RxJS error handling best practices?
Answer
- Use
catchError()for recovery. - Retry only temporary failures.
- Use
finalize()for cleanup. - Keep reusable logic in services or interceptors.
- Provide user-friendly error messages.
- Log technical details securely.
Interview Cheat Sheet
| Scenario | Recommended Operator |
|---|---|
| Recover with Default Value | catchError() |
| Throw Custom Error | throwError() |
| Retry Temporary Failure | retry() |
| Custom Retry Logic | retryWhen() |
| Cleanup | finalize() |
| Detect Slow Request | timeout() |
| Global HTTP Errors | HTTP Interceptor |
| Loading Spinner | finalize() |
| Validation Failure | throwError() |
| Network Failure | retry() |
Summary
Error handling is essential for building reliable Angular applications. RxJS provides powerful operators such as catchError(), retry(), retryWhen(), throwError(), finalize(), and timeout() to recover from failures, retry transient errors, perform cleanup, and improve user experience. Combining these operators with Angular HTTP Interceptors creates a scalable, maintainable, and enterprise-ready error handling strategy.
Key Takeaways
- ✔ Observable streams terminate after an error.
- ✔
catchError()enables graceful recovery. - ✔
throwError()creates or propagates errors. - ✔
retry()is best for temporary failures. - ✔
retryWhen()supports advanced retry strategies. - ✔
finalize()always executes cleanup logic. - ✔
timeout()detects slow operations. - ✔ Use HTTP Interceptors for cross-cutting HTTP error handling.
- ✔ Show user-friendly messages while logging technical details.
- ✔ Error handling is a core Angular and RxJS interview topic.
Next Article
➡️ 79-RxJS-Schedulers-QA.md