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 Request401→ Redirect to login403→ Redirect to unauthorized page404→ Not Found500→ Server Error0→ 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
HttpErrorResponseto inspect status, URL, response body, etc. 401commonly triggers authentication/session handling.403commonly triggers an access-denied flow.404may often require operation-specific handling.5xxshould normally produce a safe generic user message.status === 0means 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.