Skip to main content

Overview

HTTP requests can fail for many reasons: network errors, server errors, client errors, or timeouts. Angular provides robust error handling mechanisms through RxJS operators and the HttpErrorResponse class.

Error types

Angular distinguishes between two types of HTTP errors:

Client-side errors

Errors that occur in the browser:
  • Network connectivity issues
  • DNS resolution failures
  • Request cancellation
  • CORS errors
if (error.error instanceof ErrorEvent) {
  // Client-side error
  console.error('Client error:', error.error.message);
}

Server-side errors

Errors returned by the server:
  • 4xx client errors (400, 401, 403, 404, etc.)
  • 5xx server errors (500, 502, 503, etc.)
  • Invalid responses
if (error.status !== 0) {
  // Server-side error
  console.error(`Server error: ${error.status} - ${error.message}`);
}

HttpErrorResponse

The HttpErrorResponse object contains detailed error information:
import { HttpErrorResponse } from '@angular/common/http';

http.get('/api/users').subscribe({
  next: (data) => console.log(data),
  error: (error: HttpErrorResponse) => {
    console.log('Error status:', error.status);        // HTTP status code
    console.log('Error message:', error.message);      // Error message
    console.log('Error body:', error.error);           // Response body
    console.log('Error headers:', error.headers);      // Response headers
    console.log('Error url:', error.url);              // Request URL
    console.log('Error name:', error.name);            // Error name
    console.log('Error statusText:', error.statusText); // Status text
  }
});

Basic error handling

In subscriptions

Handle errors directly in the subscribe method:
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';

export class UserService {
  private http = inject(HttpClient);

  getUsers() {
    this.http.get<User[]>('/api/users').subscribe({
      next: (users) => {
        console.log('Users loaded:', users);
      },
      error: (error: HttpErrorResponse) => {
        if (error.status === 404) {
          console.error('Users not found');
        } else if (error.status === 500) {
          console.error('Server error occurred');
        } else {
          console.error('An error occurred:', error.message);
        }
      },
      complete: () => {
        console.log('Request completed');
      }
    });
  }
}

Using catchError

Handle errors in the Observable pipe:
import { catchError, of } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';

export class UserService {
  private http = inject(HttpClient);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      catchError((error: HttpErrorResponse) => {
        console.error('Error loading users:', error);
        // Return empty array as fallback
        return of([]);
      })
    );
  }
}

Centralized error handling

Error handler service

Create a service to centralize error handling logic:
error-handler.service.ts
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ErrorHandlerService {
  handleError(error: HttpErrorResponse): Observable<never> {
    let errorMessage = 'An unknown error occurred';

    if (error.error instanceof ErrorEvent) {
      // Client-side error
      errorMessage = `Client Error: ${error.error.message}`;
    } else {
      // Server-side error
      switch (error.status) {
        case 400:
          errorMessage = 'Bad Request: Please check your input';
          break;
        case 401:
          errorMessage = 'Unauthorized: Please log in';
          break;
        case 403:
          errorMessage = 'Forbidden: You do not have permission';
          break;
        case 404:
          errorMessage = 'Not Found: The requested resource does not exist';
          break;
        case 500:
          errorMessage = 'Internal Server Error: Please try again later';
          break;
        case 503:
          errorMessage = 'Service Unavailable: Server is temporarily down';
          break;
        default:
          errorMessage = `Server Error (${error.status}): ${error.message}`;
      }
    }

    console.error('HTTP Error:', errorMessage, error);
    return throwError(() => new Error(errorMessage));
  }
}
Use the service in your HTTP calls:
user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, Observable } from 'rxjs';
import { ErrorHandlerService } from './error-handler.service';

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

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      catchError(this.errorHandler.handleError)
    );
  }

  getUser(id: number): Observable<User> {
    return this.http.get<User>(`/api/users/${id}`).pipe(
      catchError(this.errorHandler.handleError)
    );
  }
}

Error interceptor

Handle errors globally with an interceptor:
error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { ToastService } from '../services/toast.service';

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

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      let errorMessage = 'An error occurred';

      if (error.error instanceof ErrorEvent) {
        // Client-side error
        errorMessage = `Error: ${error.error.message}`;
      } else {
        // Server-side error
        errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;

        // Handle specific status codes
        switch (error.status) {
          case 401:
            // Redirect to login
            router.navigate(['/login']);
            toast.error('Your session has expired. Please log in again.');
            break;
          case 403:
            toast.error('You do not have permission to perform this action.');
            break;
          case 404:
            toast.error('The requested resource was not found.');
            break;
          case 500:
            toast.error('A server error occurred. Please try again later.');
            break;
        }
      }

      console.error('HTTP Error:', errorMessage);
      return throwError(() => error);
    })
  );
};
Register the interceptor:
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { errorInterceptor } from './interceptors/error.interceptor';

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

Retry logic

Simple retry

Retry failed requests automatically:
import { retry } from 'rxjs/operators';

export class UserService {
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      retry(3), // Retry up to 3 times
      catchError(this.handleError)
    );
  }
}

Conditional retry

Retry only for specific error types:
import { retry, timer } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';

export class UserService {
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      retry({
        count: 3,
        delay: (error: HttpErrorResponse, retryCount) => {
          // Only retry on 5xx server errors
          if (error.status >= 500 && error.status < 600) {
            console.log(`Retry attempt ${retryCount}`);
            // Exponential backoff: 1s, 2s, 4s
            return timer(1000 * Math.pow(2, retryCount - 1));
          }
          // Don't retry for client errors
          throw error;
        }
      }),
      catchError(this.handleError)
    );
  }
}

Advanced retry with retryWhen

import { retryWhen, delay, take, tap } from 'rxjs/operators';
import { throwError, timer } from 'rxjs';

export class UserService {
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      retryWhen(errors =>
        errors.pipe(
          tap(error => console.log('Error occurred:', error)),
          delay(1000),      // Wait 1 second before retry
          take(3),          // Try up to 3 times
          tap(() => console.log('Retrying...'))
        )
      ),
      catchError(this.handleError)
    );
  }
}

Timeout handling

Set timeouts for requests:
import { timeout, catchError } from 'rxjs/operators';
import { TimeoutError } from 'rxjs';

export class UserService {
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      timeout(5000), // 5 second timeout
      catchError((error) => {
        if (error instanceof TimeoutError) {
          console.error('Request timed out');
          return of([]);
        }
        return throwError(() => error);
      })
    );
  }
}
Or use the timeout option in the request:
http.get<User[]>('/api/users', {
  timeout: 5000
}).pipe(
  catchError(error => {
    console.error('Request timed out or failed');
    return of([]);
  })
);

User feedback

Toast notifications

user.component.ts
import { Component, inject } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { catchError, EMPTY } from 'rxjs';
import { ToastService } from '../services/toast.service';
import { UserService } from '../services/user.service';

@Component({
  selector: 'app-users',
  template: `
    <button (click)="loadUsers()">Load Users</button>
    <div *ngFor="let user of users">
      {{ user.name }}
    </div>
  `
})
export class UsersComponent {
  private userService = inject(UserService);
  private toast = inject(ToastService);
  users: User[] = [];

  loadUsers() {
    this.userService.getUsers().pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 404) {
          this.toast.warning('No users found');
        } else if (error.status >= 500) {
          this.toast.error('Server error. Please try again later.');
        } else {
          this.toast.error('Failed to load users');
        }
        return EMPTY; // Complete the observable
      })
    ).subscribe(users => {
      this.users = users;
      this.toast.success('Users loaded successfully');
    });
  }
}

Loading states

Show loading and error states:
user.component.ts
import { Component, inject, signal } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { catchError, finalize, EMPTY } from 'rxjs';

@Component({
  selector: 'app-users',
  template: `
    @if (loading()) {
      <div>Loading...</div>
    }
    
    @if (error()) {
      <div class="error">
        {{ error() }}
        <button (click)="loadUsers()">Retry</button>
      </div>
    }
    
    @if (!loading() && !error()) {
      <div *ngFor="let user of users()">
        {{ user.name }}
      </div>
    }
  `
})
export class UsersComponent {
  private userService = inject(UserService);
  
  users = signal<User[]>([]);
  loading = signal(false);
  error = signal<string | null>(null);

  ngOnInit() {
    this.loadUsers();
  }

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

    this.userService.getUsers().pipe(
      catchError((error: HttpErrorResponse) => {
        const message = error.status === 404 
          ? 'No users found' 
          : 'Failed to load users. Please try again.';
        this.error.set(message);
        return EMPTY;
      }),
      finalize(() => this.loading.set(false))
    ).subscribe(users => {
      this.users.set(users);
    });
  }
}

Specific error scenarios

Authentication errors

import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';

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

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        // Clear session and redirect to login
        authService.logout();
        router.navigate(['/login'], {
          queryParams: { returnUrl: router.url }
        });
      }
      return throwError(() => error);
    })
  );
};

Network errors

import { catchError, of } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';

export class DataService {
  getData(): Observable<Data[]> {
    return this.http.get<Data[]>('/api/data').pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 0) {
          // Network error or CORS issue
          console.error('Network error - check your connection');
          // Return cached data if available
          const cachedData = this.getCachedData();
          return of(cachedData);
        }
        return throwError(() => error);
      })
    );
  }
}

Validation errors

Handle validation errors from the server:
import { HttpErrorResponse } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';

interface ValidationError {
  field: string;
  message: string;
}

export class FormService {
  submitForm(data: FormData): Observable<Response> {
    return this.http.post<Response>('/api/submit', data).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 422) {
          // Unprocessable entity - validation errors
          const validationErrors = error.error.errors as ValidationError[];
          validationErrors.forEach(err => {
            console.error(`Validation error on ${err.field}: ${err.message}`);
          });
        }
        return throwError(() => error);
      })
    );
  }
}

Testing error handling

user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpErrorResponse } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from './user.service';

describe('UserService Error Handling', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
        UserService
      ]
    });

    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should handle 404 error', () => {
    service.getUsers().subscribe({
      next: () => fail('should have failed'),
      error: (error: HttpErrorResponse) => {
        expect(error.status).toBe(404);
        expect(error.statusText).toBe('Not Found');
      }
    });

    const req = httpMock.expectOne('/api/users');
    req.flush('User not found', {
      status: 404,
      statusText: 'Not Found'
    });
  });

  it('should handle network error', () => {
    service.getUsers().subscribe({
      next: () => fail('should have failed'),
      error: (error: HttpErrorResponse) => {
        expect(error.error).toBeInstanceOf(ProgressEvent);
        expect(error.status).toBe(0);
      }
    });

    const req = httpMock.expectOne('/api/users');
    req.error(new ProgressEvent('Network error'));
  });
});

Best practices

Use centralized error handlingImplement a global error handler service or interceptor for consistent error handling across your application.
Provide meaningful feedbackShow user-friendly error messages instead of raw HTTP error messages.
Don’t swallow errors silently. Always log errors or notify users when something goes wrong.
Implement retry logic carefullyOnly retry idempotent operations (GET, PUT, DELETE) and avoid retrying POST requests that might create duplicate resources.
Handle different error typesDifferentiate between network errors (status 0), client errors (4xx), and server errors (5xx) for appropriate handling.
Use timeoutsAlways set reasonable timeouts to prevent requests from hanging indefinitely.

Next steps

HttpClient

Learn about making HTTP requests with HttpClient.

Interceptors

Transform requests and responses with interceptors.