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:

  1. Timeout check
  2. Retry on eligible failures
  3. Return fallback value if retries fail
  4. 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