Tutorials Logic, IN info@tutorialslogic.com

Angular Error Handling Global RxJS Patterns: Causes and Fixes

Error Boundaries

Angular applications handle expected request failures near the feature and unexpected framework errors at a final reporting boundary. Preserve the original cause, avoid duplicate notifications, and model loading, empty, stale, and retry states explicitly.

Handle an expected failure at the narrowest boundary that can recover: a form displays validation feedback, a data service maps an API failure to domain state, and a page offers retry or alternate navigation. Unexpected programming errors belong in centralized reporting, not a silent fallback.

Model loading, empty, success, stale, and error states separately. An empty successful response is not an error, and a failed refresh should not necessarily erase previously usable data.

HTTP Failures

Use RxJS catchError where the application can translate an HttpErrorResponse into a domain result, truthful fallback, or rethrown failure. Services can normalize transport details, while components still own the user-facing retry and display state.

HTTP Error Handling in a Service

HTTP Error Handling in a Service
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';

export interface User { id: number; name: string; email: string; }

@Injectable({ providedIn: 'root' })
export class UserService {
    private http = inject(HttpClient);

    getUser(id: number) {
        return this.http.get<User>(`/api/users/${id}`).pipe(
            catchError((error: HttpErrorResponse) => {
                let message = 'An unexpected error occurred.';

                if (error.status === 0) {
                    message = 'Network error - check your connection.';
                } else if (error.status === 401) {
                    message = 'Unauthorized - please log in.';
                } else if (error.status === 403) {
                    message = 'Forbidden - you do not have permission.';
                } else if (error.status === 404) {
                    message = 'User not found.';
                } else if (error.status >= 500) {
                    message = 'Server error - please try again later.';
                }

                console.error('HTTP Error:', error);
                return throwError(() => new Error(message));
            })
        );
    }
}

HTTP Error Handling

HTTP Error Handling
import { Component, inject, signal } from '@angular/core';
import { UserService, User } from './user.service';

@Component({
    selector: 'app-user',
    standalone: true,
    template: `
        @if (loading()) {
            <p>Loading...</p>
        } @else if (error()) {
            <div class="tl-alert alert-danger">{{ error() }}</div>
            <button (click)="load()">Retry</button>
        } @else if (user()) {
            <h2>{{ user()!.name }}</h2>
            <p>{{ user()!.email }}</p>
        }
    `
})
export class UserComponent {
    private userService = inject(UserService);

    user    = signal<User | null>(null);
    loading = signal(false);
    error   = signal<string | null>(null);

    ngOnInit() { this.load(); }

    load() {
        this.loading.set(true);
        this.error.set(null);

        this.userService.getUser(1).subscribe({
            next:  (u) => { this.user.set(u); this.loading.set(false); },
            error: (e) => { this.error.set(e.message); this.loading.set(false); }
        });
    }
}

HTTP Error Interceptors

An interceptor can translate transport failures that have one application-wide policy, such as expiring an authenticated session or attaching a correlation identifier. Keep feature-specific messages and retry decisions near the feature that understands the operation.

Do not show a toast in the interceptor and another message in the component for the same failure. A 401 flow must exclude the sign-in or refresh request, coordinate concurrent failures, preserve the attempted URL when appropriate, and stop cleanly when refresh fails.

  • Return the error Observable so cancellation and downstream handling remain intact.
  • Check the request origin before applying authentication or organization-specific policy.
  • Use HttpContext to opt exceptional requests out of a global policy.

Global HTTP Error Interceptor

Global HTTP Error Interceptor
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
    const router = inject(Router);

    return next(req).pipe(
        catchError((error: HttpErrorResponse) => {
            if (error.status === 401) {
                // Redirect to login on unauthorized
                router.navigate(['/login']);
            }

            if (error.status === 403) {
                router.navigate(['/forbidden']);
            }

            if (error.status >= 500) {
                console.error('Server error:', error.message);
                // Could show a toast notification here
            }

            return throwError(() => error);
        })
    );
};

HTTP Interceptor for Global Error Handling

HTTP Interceptor for Global Error Handling
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { errorInterceptor } from './error.interceptor';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
    providers: [
        provideRouter(routes),
        provideHttpClient(
            withInterceptors([errorInterceptor])
        )
    ]
};

Global Error Reporting

A custom ErrorHandler can normalize and send unexpected errors to the project monitoring boundary, but it should avoid throwing another error or exposing stack traces and private data to users.

Capture release version, route template, browser context, and a correlation identifier where policy allows. Redact tokens, request bodies, personal data, and arbitrary user-entered values before transmission.

Global reporting is the last line of visibility, not a recovery strategy. Keep local error states and retry decisions near the operation that understands them.

Global ErrorHandler

Global ErrorHandler
import { ErrorHandler, Injectable, inject } from '@angular/core';

abstract class ErrorReporter {
    abstract capture(event: {
        name: string;
        message: string;
        stack?: string;
    }): void;
}

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
    private readonly reporter = inject(ErrorReporter);

    handleError(error: unknown): void {
        const normalized = error instanceof Error
            ? error
            : new Error('Unknown application error', { cause: error });

        try {
            this.reporter.capture({
                name: normalized.name,
                message: normalized.message,
                stack: normalized.stack
            });
        } catch (reportingFailure) {
            console.error('Error reporting failed', reportingFailure);
        }

        console.error(normalized);
    }
}

Global Error Handler

Global Error Handler
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { provideRouter } from '@angular/router';
import { GlobalErrorHandler } from './global-error-handler';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
    providers: [
        provideRouter(routes),
        // Override the default ErrorHandler
        { provide: ErrorHandler, useClass: GlobalErrorHandler }
    ]
};

Template Error States

Use built-in control flow with an explicit view model to render loading, stale, empty, failure, and success states. Keep the previous value visible during a failed refresh when that is safer than replacing usable data with a blank screen.

The @defer @error block handles failure to load that deferred dependency. It does not catch arbitrary exceptions thrown later by the rendered component, and a route-level not-found page does not replace an API-specific error state.

Approach Best for
Signal error state + @if Component-level HTTP errors
HTTP Interceptor Global auth errors (401, 403)
ErrorHandler Unhandled JS exceptions, crash reporting
@defer @error Lazy-loaded component failures
Route-level error pages 404, 500 pages
  • Put retry beside the operation that can be repeated safely.
  • Move focus or announce an error when a dynamic update would otherwise be missed by assistive technology.
  • Never render raw stack traces, backend exception text, or request identifiers as the user-facing message.
Confirm the page outcome

Failure Review

5 checks
  • I distinguish expected domain failures from unexpected programming errors and recover at the narrowest capable boundary.
  • I preserve network, HTTP status, empty-success, stale-data, cancellation, and decoding states instead of flattening them.
  • Only one layer owns each user notification, authentication transition, retry policy, and diagnostic report.
  • Global reports redact sensitive data and cannot throw a second error or force an automatic reload loop.
  • I test success, backend failure, network failure, retry, cancellation, stale refresh, and reporting failure paths.

Failure Review Questions

catchError replaces the failed stream with whatever Observable it returns. Returning of([]) turns the failure into a successful empty result, while throwError preserves an error path for callers.

The interceptor displays a notification and the component or service also handles the rethrown error. Decide which layer owns user-facing feedback.

Some exceptions are recoverable or confined to one optional feature. Redirecting the whole application can destroy unsaved work and create a navigation loop if the error page triggers the same fault.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.