A robust application is defined not by how it works in perfect conditions, but by how it recovers when things go wrong.
1The catchError Pattern
The catchError operator is your primary line of defense. By piping your HTTP request through it, you create a recovery strategy. This strategy can log the error to a monitoring service, transform the raw technical error into a user-friendly message, or even provide default data to keep the UI functioning. Crucially, catchError must return a new Observable—usually created via throwError—to correctly propagate the failure state to the final subscriber.
2Diagnosing the Failure
Not all errors are created equal. Angular provides the HttpErrorResponse object to help you differentiate. A status code of 0 typically indicates a client-side or network error (like being offline), while status codes like 404 or 500 indicate that the backend was reached but couldn't fulfill the request. Understanding this distinction is vital for providing accurate feedback to the user—for example, telling them to 'check their connection' versus 'contact support'.
3Step-by-Step Breakdown
In the real world, things go wrong. Servers crash, networks fail. Your app needs to handle these moments gracefully.
When an HTTP request fails, it emits an error into the Observable stream. If we don't catch it, the stream dies.
But a better way is to use the RxJS 'catchError' operator. This lets us handle the error before it even reaches our component.
Checkpoint: Which RxJS operator is used to intercept and handle errors in an Observable stream?
- →map
- →catchError
Inside the handler, we use 'HttpErrorResponse'. It helps us tell if the error is a client-side problem (network) or a server-side problem.
Finally, we return a new error using 'throwError'. This passes a user-friendly message to the final subscriber.
Checkpoint: If you want to notify the UI about an error after handling it in a service, what should you return from catchError?
- →null
- →throwError(factory)
Resilience built! Your app is now ready for the unpredictable nature of the internet.
Next, we'll dive deep into the power of RxJS and Observables beyond just HTTP.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Error Messages Must Be Announced, Not Just Displayed
Rendering an error message in a `<div>` that appears after a failed request doesn't guarantee a screen reader announces it — use `role="alert"` or an `aria-live="assertive"` region so the error is proactively spoken, since the user isn't necessarily focused near where it appears.
2Distinguish Genuinely Different Failure States in Both Text and Structure
A network failure, a 404, and a 500 server error are different problems with different user actions available (retry vs. contact support) — collapsing them all into one generic 'Something went wrong' message denies users the specific information they need to act.
SEO Implications
- 1
Unhandled Client-Side HTTP Errors Can Leave a Page in a Broken, Empty State for Crawlers Too
If a page's primary content depends on an API call that fails and there's no fallback UI, a crawler evaluating that page (even under SSR, if the same request fails server-side) sees an empty or broken page — robust error handling protects both real users and crawl quality.
- 2
Never Expose Raw Server Error Details in Client-Facing Error Messages
Beyond being a poor user experience, echoing raw stack traces or internal error details directly to the UI can leak information useful to attackers — always map backend errors to safe, generic user-facing messages.
Best Practices
Use `catchError` in RxJS Pipes to Handle Errors Declaratively
Wrapping HTTP calls with `.pipe(catchError(err => ...))` keeps error-handling logic co-located with the request itself, rather than scattering try/catch-style logic across every place the observable happens to be consumed.
Differentiate Between Retryable and Non-Retryable Errors
A transient network timeout is often worth automatically retrying (via RxJS's `retry` operator); a 401 Unauthorized or 403 Forbidden should never be retried the same way, since retrying won't change the outcome and may look like a bug to the user.
Frequent Bugs
An HTTP error silently fails, leaving the UI in an infinite loading spinner state.
The observable chain has no `catchError` (or equivalent) handling — when an HTTP request errors, an unhandled observable simply terminates without notifying the subscriber of anything beyond the error itself, which if unobserved can leave any loading-state flag stuck as true forever. Add explicit error handling that resets loading state and surfaces the failure.
A generic error interceptor accidentally swallows more specific error handling logic in individual components.
The global `HttpInterceptor` is catching and handling the error (e.g., converting it to a default value) before it ever reaches the component's own `catchError`. Either let the interceptor re-throw errors it doesn't specifically handle, or move component-specific error logic ahead of the generic interceptor's handling.
Real-World Examples
Accessible, Differentiated HTTP Error Handling
A data-fetching service catches HTTP errors, maps them to safe, specific user-facing messages, and the component surfaces them via an ARIA alert region so all users are informed of exactly what went wrong.
this.http.get('/api/data').pipe(
catchError(err => {
const message = err.status === 404 ? 'Data not found.' : 'Something went wrong. Please try again.';
return of({ error: message });
})
);