In this article we going to learn how to handle API errors globally in Angular so you don't have to write the same error handling code in every single service or component.
If you've built any Angular app that talks to a backend, you've probably written something like this at least a dozen times — .subscribe({ error: err => console.log(err) }). Every service, every HTTP call, same thing repeated. And then when the design team says "show a toast notification on every API error", you have to go touch every single one of those places. That's not how it should work.
The right approach is to handle errors in one place. Angular gives you a couple of clean ways to do this — HTTP Interceptors for catching HTTP errors, and ErrorHandler for catching everything else. Together they cover pretty much every error scenario in an Angular app.
This tutorial shows how to:
- Create a global HTTP error interceptor to catch all API errors
- Handle specific HTTP status codes like 401, 403, 404, 500
- Show error notifications using a toast service
- Use Angular's ErrorHandler class to catch unexpected JS errors
- Log errors to an external service for monitoring
- Avoid duplicate error handling in components and services
Why Global Error Handling
Let me paint the picture. You have 20 API calls across 10 services. Each one has its own error block. Product owner says — on every 500 error, show a "Something went wrong, try again" message. On 401, logout the user. On 403, show "You don't have permission".
Without global handling you'd update 20 places. With global handling you update one file.
Also — unhandled errors in Angular don't always crash the app visibly. Sometimes they just silently fail and the user stares at a blank section wondering what happened. Global error handling makes sure nothing slips through.
Step 1 : Create a Toast Notification Service
Before building the interceptor, you need something to actually show the error messages to the user. A simple toast service works well for this.
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
export interface Toast {
message: string;
type: 'error' | 'success' | 'warning';
}
@Injectable({ providedIn: 'root' })
export class ToastService {
private toastSubject = new Subject<Toast>();
toast$ = this.toastSubject.asObservable();
showError(message: string) {
this.toastSubject.next({ message, type: 'error' });
}
showSuccess(message: string) {
this.toastSubject.next({ message, type: 'success' });
}
showWarning(message: string) {
this.toastSubject.next({ message, type: 'warning' });
}
}
Then in your AppComponent or a dedicated toast component, subscribe to toast$ and display the notifications. If you're using Ionic, you can use ion-toast here instead. If it's a pure Angular app, a simple fixed-position div works fine.
Step 2 : Create the Global Error Interceptor
This is where all HTTP errors get caught. Create a new file error.interceptor.ts :
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
import { ToastService } from './toast.service';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const toastService = inject(ToastService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
let errorMessage = 'Something went wrong. Please try again.';
switch (error.status) {
case 0:
errorMessage = 'No internet connection. Check your network.';
break;
case 400:
errorMessage = error.error?.message || 'Bad request. Check your input.';
break;
case 401:
errorMessage = 'Session expired. Please login again.';
localStorage.removeItem('token');
router.navigate(['/login']);
break;
case 403:
errorMessage = 'You do not have permission to do this.';
router.navigate(['/unauthorized']);
break;
case 404:
errorMessage = 'The requested resource was not found.';
break;
case 500:
errorMessage = 'Server error. We are working on it.';
break;
case 503:
errorMessage = 'Service unavailable. Try again later.';
break;
default:
errorMessage = `Unexpected error (${error.status}). Please try again.`;
}
toastService.showError(errorMessage);
return throwError(() => error);
})
);
};
The switch handles each common status code with a user-friendly message. Status 0 means the request never reached the server — usually no internet or CORS issue. That one trips people up because it's not an HTTP status code from the server, it's a network-level failure.
Notice throwError() at the end. This re-throws the error after showing the toast. Why? Because sometimes your component's error handler needs to know the request failed — maybe to reset a loading flag or update some local state. If you swallow the error in the interceptor, the component never finds out.
Step 3 : Register the Error Interceptor
For standalone Angular apps, register it in main.ts alongside any other interceptors :
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { authInterceptor } from './app/auth.interceptor';
import { errorInterceptor } from './app/error.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor, errorInterceptor])
)
]
}).catch(err => console.error(err));
Order matters here. authInterceptor adds the token to the request first, then errorInterceptor handles any errors that come back. That's the right order.
For NgModule apps, register it in app.module.ts :
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true
}
]
Step 4 : Use Angular's Global ErrorHandler
HTTP interceptors catch HTTP errors. But what about JavaScript errors? A null reference exception, a TypeError, something that throws inside a component — those don't go through HttpClient at all.
Angular has a built-in ErrorHandler class for exactly this. Override it to catch all unhandled errors in the app :
import { ErrorHandler, Injectable, inject } from '@angular/core';
import { ToastService } from './toast.service';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
private toastService = inject(ToastService);
handleError(error: any): void {
// Log to console in development
console.error('Global Error:', error);
// Show user-friendly message
const message = error?.message || 'An unexpected error occurred.';
// Don't show toast for HTTP errors — those are handled by the interceptor
if (!error?.status) {
this.toastService.showError('Something went wrong. Please refresh the page.');
}
// Optionally log to an error tracking service
// this.logErrorToServer(error);
}
}
The check !error?.status prevents double-showing errors. HTTP errors have a status property, so they're already handled by the interceptor. This handler catches everything else.
Register it in your providers :
// Standalone - main.ts
bootstrapApplication(AppComponent, {
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))
]
});
// NgModule - app.module.ts
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
]
Step 5 : Log Errors to a Monitoring Service
Showing errors to the user is one thing. But you also want to know about errors happening in production. That's where error logging comes in.
Services like Sentry, Datadog, or even a simple custom endpoint on your own backend can capture these errors. Here's how to send errors to your own API from the GlobalErrorHandler :
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
private http = inject(HttpClient);
private toastService = inject(ToastService);
handleError(error: any): void {
console.error('Global Error:', error);
if (!error?.status) {
this.toastService.showError('Something went wrong. Please refresh the page.');
}
// Log to your error tracking endpoint
this.logError(error);
}
private logError(error: any): void {
const errorPayload = {
message: error?.message || 'Unknown error',
stack: error?.stack || '',
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
};
// Fire and forget — don't let logging errors break the app
this.http.post('/api/errors/log', errorPayload).subscribe({
error: () => {} // Silently fail if logging itself fails
});
}
}
The error payload includes the stack trace, timestamp, current URL, and user agent. That information makes debugging production issues much easier — you see exactly where the error happened and what browser the user was on.
Note the empty error handler in the subscribe — if the logging call itself fails, you don't want that to trigger another error which triggers another log attempt and so on.
Step 6 : Clean Up Components — Stop Repeating Error Logic
Now that global handling is in place, your components and services can be much cleaner. You don't need error handling in every subscribe call anymore.
Before — messy :
this.http.get('/api/users').subscribe({
next: data => this.users = data,
error: err => {
console.error(err);
this.toastService.showError('Failed to load users');
this.loading = false;
}
});
After — clean :
this.http.get('/api/users').subscribe({
next: data => {
this.users = data;
this.loading = false;
},
error: () => {
// Global interceptor already showed the toast
// Just reset local state here
this.loading = false;
}
});
The error block still exists — you need it to reset the loading flag. But you're not showing toasts, not logging, not figuring out what the status code means. The interceptor already did all of that. The component just handles its own local state cleanup.
Summary
You learned how to handle API errors globally in Angular. You covered :
- Creating a ToastService to show user-friendly error notifications
- Building a global error interceptor with specific messages for 0, 400, 401, 403, 404, 500, 503 status codes
- Registering the interceptor in standalone and NgModule apps
- Using Angular's ErrorHandler class to catch JavaScript errors outside of HTTP calls
- Logging errors to a backend monitoring endpoint with stack trace and user context
- Cleaning up components so they no longer repeat error handling logic
The pattern is simple — interceptor handles HTTP errors, ErrorHandler handles everything else, and components just deal with local state. Once this is set up, adding a new API call is clean and your error handling stays consistent across the entire app.
I hope you like this article...
Happy coding! 🚀
0 Comments