← Back to Articles         
How do you globally handle HTTP errors?

How do you globally handle HTTP errors?

Published on 02 Oct 2026     9 min read Angular
Error Handling

How Do You Globally Handle HTTP Errors in Angular?

1. What is Global HTTP Error Handling?

Global HTTP error handling means handling common API errors in one central place instead of writing catchError() repeatedly in every Angular service or component.

In modern Angular, the usual approach is a functional HTTP interceptor.

Component
    ↓
Service
    ↓
HTTP Error Interceptor
    ↓
ASP.NET Core Web API

When the API responds with an error:

ASP.NET Core API
    ↓
401 / 403 / 404 / 500
    ↓
HTTP Error Interceptor
    ↓
Handle error globally
    ↓
Component

2. Full Simple Sample Program

This example handles:

  • 400 → Bad Request
  • 401 → Redirect to login
  • 403 → Redirect to unauthorized page
  • 404 → Not Found
  • 500 → Server Error
  • 0 → Network/CORS/server-unreachable type failure

error.interceptor.ts

import {
  HttpErrorResponse,
  HttpInterceptorFn
} 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) => {

        let errorMessage = 'Something went wrong.';

        switch (error.status) {

          case 0:
            errorMessage = 'Unable to connect to the server.';
            break;

          case 400:
            errorMessage = 'Invalid request.';
            break;

          case 401:
            errorMessage = 'Your session has expired. Please login again.';

            router.navigate(['/login']);
            break;

          case 403:
            errorMessage = 'You are not authorized to access this resource.';

            router.navigate(['/unauthorized']);
            break;

          case 404:
            errorMessage = 'Requested resource was not found.';
            break;

          case 500:
            errorMessage = 'Server error. Please try again later.';
            break;

          default:
            errorMessage = 'An unexpected error occurred.';
        }

        console.error('HTTP Error:', {
          status: error.status,
          message: errorMessage,
          url: error.url
        });

        return throwError(
          () => new Error(errorMessage)
        );

      })

    );

  };

app.config.ts

Register the interceptor globally.

import {
  ApplicationConfig
} from '@angular/core';

import {
  provideHttpClient,
  withInterceptors
} from '@angular/common/http';

import {
  provideRouter
} from '@angular/router';

import {
  routes
} from './app.routes';

import {
  errorInterceptor
} from './error.interceptor';

export const appConfig: ApplicationConfig = {

  providers: [

    provideRouter(routes),

    provideHttpClient(
      withInterceptors([
        errorInterceptor
      ])
    )

  ]

};

app.routes.ts

import { Routes } from '@angular/router';

import { Products } from './products';
import { Login } from './login';
import { Unauthorized } from './unauthorized';

export const routes: Routes = [

  {
    path: 'products',
    component: Products
  },

  {
    path: 'login',
    component: Login
  },

  {
    path: 'unauthorized',
    component: Unauthorized
  }

];

product.ts

export interface Product {

  id: number;

  name: string;

  price: number;

}

product.service.ts

Notice that we don't need to repeat global error handling here.

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';

import { Product } from './product';

@Injectable({
  providedIn: 'root'
})
export class ProductService {

  private http = inject(HttpClient);

  private apiUrl =
    'https://localhost:7001/api/products';

  getProducts(): Observable<Product[]> {

    return this.http.get<Product[]>(
      this.apiUrl
    );

  }

}

products.ts

import {
  Component,
  inject
} from '@angular/core';

import {
  ProductService
} from './product.service';

import {
  Product
} from './product';

@Component({
  selector: 'app-products',
  standalone: true,
  templateUrl: './products.html'
})
export class Products {

  private productService =
    inject(ProductService);

  products: Product[] = [];

  errorMessage = '';

  loadProducts() {

    this.errorMessage = '';

    this.productService
      .getProducts()
      .subscribe({

        next: (data) => {

          this.products = data;

        },

        error: (error) => {

          this.errorMessage =
            error.message;

        }

      });

  }

}

products.html

<h2>Products</h2>

<button (click)="loadProducts()">
  Load Products
</button>

@if (errorMessage) {

  <p>
    {{ errorMessage }}
  </p>

}

@for (product of products; track product.id) {

  <div>
    {{ product.name }}
    -
    {{ product.price }}
  </div>

}

3. How Does It Work?

Suppose Angular makes this request:

this.http.get<Product[]>(
  'https://localhost:7001/api/products'
);

The interceptor automatically sees the request:

Products Component
       ↓
ProductService
       ↓
errorInterceptor
       ↓
ASP.NET Core API

If the API succeeds:

API
 ↓
200 OK
 ↓
Interceptor
 ↓
Service
 ↓
Component

If the API fails:

API
 ↓
401 Unauthorized
 ↓
Interceptor
 ↓
catchError()
 ↓
router.navigate(['/login'])

The component/service doesn't need to know how every global error should be handled.


4. Understanding HttpInterceptorFn

A modern functional interceptor looks like:

export const errorInterceptor: HttpInterceptorFn =
  (req, next) => {

    return next(req);

  };

There are two important parameters.

req

Represents the outgoing HTTP request.

req.url
req.method
req.headers
req.body

For example:

console.log(req.url);

next

next(req) passes the request to the next interceptor or ultimately to the HTTP backend.

return next(req);

The returned Observable contains the HTTP response or error.

Therefore:

return next(req).pipe(
  catchError(...)
);

means:

Send the request, and if the HTTP Observable fails, process that error.


5. Handling Different Status Codes

400 — Bad Request

Usually indicates invalid input or validation failure.

case 400:

  errorMessage = 'Invalid request.';

  break;

For ASP.NET Core validation errors, you may want to display the specific field-level validation messages rather than replacing everything with "Invalid request".


6. Handling 401 Unauthorized

A 401 usually means authentication is missing, invalid, or expired.

For example:

JWT expired

The interceptor can redirect:

case 401:

  router.navigate(['/login']);

  break;

In a JWT application, you may also clear invalid authentication state before redirecting.

401
 ↓
Error Interceptor
 ↓
Clear invalid authentication state
 ↓
Redirect to Login

If your application uses refresh tokens, the flow can be more sophisticated:

401
 ↓
Attempt Refresh Token
 ↓
Refresh successful?
   ↙       ↘
 YES       NO
  ↓         ↓
Retry     Login
Request

7. Handling 403 Forbidden

403 normally means the server understood the request but refuses access.

For example:

User Role = User

Required Role = Admin

ASP.NET Core may return:

403 Forbidden

Angular can redirect:

case 403:

  router.navigate(['/unauthorized']);

  break;

8. Handling 404

case 404:

  errorMessage =
    'Requested resource was not found.';

  break;

For example:

GET /api/products/99999

If product 99999 doesn't exist:

404 Not Found

9. Handling 500

A 500 represents an internal server-side failure.

case 500:

  errorMessage =
    'Server error. Please try again later.';

  break;

Don't normally show internal exception details to users.

Avoid displaying things such as:

SqlException...
NullReferenceException...
StackTrace...
Database connection...

Instead show a safe message.


10. What Does Status 0 Mean?

Angular may report:

error.status === 0

when it didn't receive a normal HTTP response.

Possible causes include:

  • Network problem
  • API server unavailable
  • CORS blocking
  • Connection failure
  • TLS/certificate issues

Handle it:

case 0:

  errorMessage =
    'Unable to connect to the server.';

  break;

But status === 0 does not tell you the exact cause. Check the browser Network tab and console.


11. Why Return throwError()?

After global processing:

return throwError(
  () => new Error(errorMessage)
);

we propagate the error.

Therefore, the component can still do:

.subscribe({

  next: data => {
    this.products = data;
  },

  error: error => {
    this.errorMessage = error.message;
  }

});

This gives you two levels of handling:

Interceptor
    ↓
Global handling
    ↓
Component
    ↓
UI-specific handling

12. Global vs Local Error Handling

You generally should not put every error-handling decision in the interceptor.

Use the interceptor for common behavior and local code for operation-specific behavior.

Error handling Best place
Expired authentication / common 401 handling Interceptor
Global 403 behavior Interceptor
Generic server-error handling Interceptor
HTTP error logging Interceptor
Product not found Product service/component
Form validation errors Form/component
Duplicate product Product service/component
User-facing field validation Component

A useful architecture is:

               HTTP Request
                    ↓
               Interceptor
                    ↓
               Web API
                    ↓
               HTTP Error
                    ↓
        ┌───────────┴───────────┐
        ↓                       ↓
Global handling           Local handling
        ↓                       ↓
401 / 403 / 500       Business-specific errors
        ↓                       ↓
Interceptor             Service / Component

13. Multiple Interceptors

Yes. A real Angular application can have multiple interceptors.

For example:

authInterceptor
      ↓
correlationIdInterceptor
      ↓
loggingInterceptor
      ↓
errorInterceptor
      ↓
ASP.NET Core API

Register them:

provideHttpClient(
  withInterceptors([
    authInterceptor,
    correlationIdInterceptor,
    loggingInterceptor,
    errorInterceptor
  ])
)

This keeps each interceptor focused on one cross-cutting concern.


Key Points

  • Use an HTTP interceptor for global HTTP error handling.
  • Modern Angular supports functional interceptors using HttpInterceptorFn.
  • Register functional interceptors using withInterceptors().
  • Use RxJS catchError() to intercept failed HTTP Observables.
  • Use HttpErrorResponse to inspect status, URL, response body, etc.
  • 401 commonly triggers authentication/session handling.
  • 403 commonly triggers an access-denied flow.
  • 404 may often require operation-specific handling.
  • 5xx should normally produce a safe generic user message.
  • status === 0 means no normal HTTP response was received; investigate network/CORS/connectivity.
  • Use throwError() when the error should continue downstream.
  • Keep global/common handling in interceptors and business/UI-specific handling in services/components.
  • Never rely on Angular authorization alone—ASP.NET Core Web API must enforce authentication and authorization.

Interview Questions and Answers

1. How do you globally handle HTTP errors in Angular?

Use an HTTP interceptor. The interceptor can use catchError() to inspect HttpErrorResponse and centrally handle common errors such as 401, 403, and 500.

return next(req).pipe(
  catchError(error => {
    // Global handling

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

2. Why use an interceptor instead of handling errors in every service?

It centralizes common error handling and avoids duplicated code across services.

Without interceptor:
Service A → catchError()
Service B → catchError()
Service C → catchError()

With interceptor:
All Services → Error Interceptor

3. What is HttpInterceptorFn?

HttpInterceptorFn is Angular's functional HTTP interceptor type.

export const errorInterceptor: HttpInterceptorFn =
  (req, next) => {

    return next(req);

  };

It can inspect or transform HTTP requests and responses.

4. How do you register a functional interceptor?

provideHttpClient(
  withInterceptors([
    errorInterceptor
  ])
)

5. How do you redirect on a 401?

Inject Router inside the functional interceptor:

const router = inject(Router);

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

6. How do you handle 403 globally?

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

This is a UI response to the backend's authorization decision.

7. Should all errors be handled globally?

No. Common cross-cutting HTTP behavior belongs in the interceptor, while operation-specific errors should usually remain in the service/component.

For example:

401 → Interceptor
403 → Interceptor

Product 404 → Product component/service

Form validation 400
        → Form component

8. Can Angular have multiple HTTP interceptors?

Yes.

withInterceptors([
  authInterceptor,
  loggingInterceptor,
  errorInterceptor
])

Each interceptor can handle a separate concern.

9. What is the typical production flow?

Angular Component
       ↓
Service
       ↓
Auth Interceptor
       ↓
Correlation ID Interceptor
       ↓
Error/Logging Interceptor
       ↓
ASP.NET Core Web API
       ↓
Global Exception Handling
       ↓
HTTP Status + Safe Error Response
       ↓
Angular Interceptor
       ↓
Component / User

This is a common structure for an Angular + ASP.NET Core Web API application.

← Back to Articles