In modern web applications, handling HTTP communication is a core responsibility. Whether it’s fetching data from an API, sending user inputs to a server, or managing authentication tokens, repetitive HTTP-related tasks can clutter your codebase. Angular interceptors solve this problem by providing a centralized way to intercept, modify, and handle HTTP requests and responses.
In this blog, we’ll dive deep into Angular interceptors: what they are, how they work, common use cases with practical examples, advanced techniques, best practices, and troubleshooting tips. By the end, you’ll be able to leverage interceptors to streamline your app’s HTTP logic and write cleaner, more maintainable code.
Table of Contents
- Introduction to Angular Interceptors
- What Are Angular Interceptors?
- How Interceptors Work in Angular
- Setting Up Your First Interceptor
- Key Use Cases with Examples
- Adding Headers (e.g., Authentication Tokens)
- Error Handling
- Request/Response Transformation
- Logging
- Caching
- Advanced Interceptor Techniques
- Best Practices
- Troubleshooting Common Issues
- Conclusion
- References
What Are Angular Interceptors?
Formally, an Angular interceptor is a class that implements the HttpInterceptor interface from @angular/common/http. This interface requires a single method: intercept, which takes two arguments:
req: HttpRequest<any>: The outgoing request object (immutable).next: HttpHandler: A handler object that passes the request to the next interceptor in the chain (or to the server if it’s the last interceptor).
The intercept method must return an Observable<HttpEvent<any>>, which represents the server’s response (or an error).
How Interceptors Work in Angular
Angular processes HTTP requests through a chain of interceptors. Here’s a step-by-step breakdown of the flow:
- Request Initiation: A component or service calls
HttpClient(e.g.,http.get('api/data')). - Interceptor Chain: The request passes through each interceptor in the order they’re provided. Each interceptor can:
- Modify the request (e.g., add headers, transform the body).
- Short-circuit the chain (e.g., return cached data instead of hitting the server).
- Pass the request to the next interceptor via
next.handle(modifiedReq).
- Server Communication: After passing through all interceptors, the final request is sent to the server.
- Response Handling: The server’s response flows back through the interceptor chain in reverse order. Each interceptor can modify the response (e.g., parse data, log metrics) or handle errors.
- Delivery to Subscriber: The processed response (or error) is delivered to the original subscriber (the component/service that initiated the request).

Image source: Angular Official Documentation
Setting Up Your First Interceptor
Let’s create a simple interceptor to add a X-App-Version header to all requests.
Step 1: Create the Interceptor Class
Generate a new interceptor using Angular CLI:
ng generate interceptor version
This creates version.interceptor.ts in your src/app directory. Open it and implement the intercept method:
// version.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class VersionInterceptor implements HttpInterceptor {
constructor() {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Clone the request to modify it (since HttpRequest is immutable)
const modifiedReq = request.clone({
headers: request.headers.set('X-App-Version', '1.0.0')
});
// Pass the modified request to the next handler
return next.handle(modifiedReq);
}
}
Step 2: Provide the Interceptor
To register the interceptor, add it to the providers array of your root module (app.module.ts). Use the HTTP_INTERCEPTORS token from @angular/common/http:
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppComponent } from './app.component';
import { VersionInterceptor } from './version.interceptor';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: VersionInterceptor,
multi: true // Allows multiple interceptors
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
The multi: true flag is critical—it tells Angular that HTTP_INTERCEPTORS is a multi-provider token, allowing multiple interceptors to be registered.
Key Use Cases with Examples
Let’s explore common scenarios where interceptors shine, with practical code examples.
1. Adding Headers (e.g., Authentication Tokens)
A frequent use case is adding headers like Authorization (for JWT tokens) or Content-Type to requests.
Example: Auth Token Interceptor
This interceptor adds a JWT token from localStorage to the Authorization header:
// auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Get token from localStorage
const token = localStorage.getItem('authToken');
if (token) {
// Clone the request and add the token
const authReq = request.clone({
headers: request.headers.set('Authorization', `Bearer ${token}`)
});
return next.handle(authReq);
}
// If no token, pass the original request
return next.handle(request);
}
}
Register the Interceptor: Add it to app.module.ts providers:
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: VersionInterceptor, multi: true } // Existing interceptor
]
2. Error Handling
Interceptors can centralize error handling for HTTP requests (e.g., 401 Unauthorized, 500 Server Error).
Example: Error Handling Interceptor
This interceptor catches errors, logs them, and redirects to the login page on 401 errors:
// error.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';
import { ToastService } from './toast.service'; // Assume a service for showing toasts
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private router: Router, private toast: ToastService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
// Log error details
console.error('HTTP Error:', error);
// Handle specific error statuses
switch (error.status) {
case 401:
// Unauthorized: Clear token and redirect to login
localStorage.removeItem('authToken');
this.router.navigate(['/login']);
this.toast.show('Session expired. Please log in again.');
break;
case 404:
this.toast.show('Resource not found.');
break;
case 500:
this.toast.show('Server error. Please try again later.');
break;
}
// Re-throw the error to let the subscriber handle it (optional)
return throwError(() => new Error(error.message || 'Unknown error occurred'));
})
);
}
}
3. Request/Response Transformation
Interceptors can modify request bodies (e.g., format data) or response data (e.g., parse nested JSON).
Example: Date Formatting Interceptor
This interceptor converts ISO date strings in the response to Date objects:
// date.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class DateInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// Transform response body
this.convertDates(event.body);
}
return event;
})
);
}
private convertDates(body: any): void {
if (body && typeof body === 'object') {
for (const key in body) {
if (body.hasOwnProperty(key)) {
const value = body[key];
// Check if value is an ISO date string
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
body[key] = new Date(value); // Convert to Date object
} else if (typeof value === 'object') {
this.convertDates(value); // Recurse for nested objects
}
}
}
}
}
}
4. Logging
Track request details (method, URL, duration) for debugging or analytics.
Example: Logging Interceptor
// logging.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const startTime = Date.now();
console.log(`Request: ${request.method} ${request.urlWithParams}`);
return next.handle(request).pipe(
tap({
next: (event) => {
const duration = Date.now() - startTime;
console.log(`Response: ${request.method} ${request.urlWithParams} (${duration}ms)`);
},
error: (error) => {
const duration = Date.now() - startTime;
console.error(`Error: ${request.method} ${request.urlWithParams} (${duration}ms)`, error);
}
})
);
}
}
5. Caching
Reduce redundant requests by caching responses for a set duration.
Example: Caching Interceptor
// cache.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpResponse
} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
interface CacheEntry {
data: any;
timestamp: number;
}
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private cache = new Map<string, CacheEntry>();
private cacheDuration = 5 * 60 * 1000; // 5 minutes
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Only cache GET requests
if (request.method !== 'GET') {
return next.handle(request);
}
const cacheKey = request.urlWithParams;
const cachedEntry = this.cache.get(cacheKey);
// Return cached data if it exists and is not expired
if (cachedEntry && Date.now() - cachedEntry.timestamp < this.cacheDuration) {
console.log(`Returning cached data for ${cacheKey}`);
return of(new HttpResponse({ body: cachedEntry.data }));
}
// Fetch fresh data and cache it
return next.handle(request).pipe(
tap((event) => {
if (event instanceof HttpResponse) {
this.cache.set(cacheKey, {
data: event.body,
timestamp: Date.now()
});
}
})
);
}
}
Advanced Interceptor Techniques
Multi-Interceptor Setup
You can register multiple interceptors (e.g., AuthInterceptor → LoggingInterceptor → ErrorInterceptor). The order in the providers array determines the request flow (first provider runs first).
// app.module.ts providers
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, // 1st
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, // 2nd
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true } // 3rd
]
Conditional Interception
Intercept only specific requests using request.url or request.headers:
// Only intercept requests to '/api'
if (request.url.startsWith('/api')) {
// Modify request
}
return next.handle(request);
Async Operations in Interceptors
Use RxJS operators like mergeMap to handle async tasks (e.g., fetching a token from an async storage service):
import { mergeMap } from 'rxjs/operators';
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.authService.getToken().pipe( // Assume getToken() returns Observable<string>
mergeMap((token) => {
const authReq = request.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
return next.handle(authReq);
})
);
}
Best Practices
-
Keep Interceptors Focused: Each interceptor should handle one responsibility (e.g., auth headers, error handling). Avoid “god interceptors” that do everything.
-
Avoid Side Effects: Interceptors should be pure functions. Avoid modifying external state (e.g.,
localStorage) unless necessary. -
Provide Interceptors at the Root: Register interceptors in the root module (
providers: [{ provide: HTTP_INTERCEPTORS, ... }]) to apply them app-wide. Avoid providing them in feature modules unless you want scoped behavior. -
Handle Async Correctly: Use RxJS operators like
mergeMap,switchMap, orcatchErrorfor async logic—never useasync/awaitdirectly inintercept. -
Test Interceptors: Write unit tests for interceptors using
HttpClientTestingModuleto verify they modify requests/responses as expected.
Troubleshooting Common Issues
Interceptors Not Running
- Check Provider Setup: Ensure
multi: trueis set in the provider. - Order Matters: If an interceptor short-circuits the chain (e.g., returns cached data), subsequent interceptors won’t run.
Infinite Loops
- Caused by modifying the request in a way that re-triggers the same interceptor (e.g., cloning a request with the same URL in a caching interceptor). Use conditional checks to avoid this.
Token Not Added to Headers
- Ensure the token exists in storage when the interceptor runs. If using async storage, use
mergeMapto wait for the token.
Conclusion
Angular interceptors are a powerful tool for centralizing HTTP logic. By leveraging them, you can simplify authentication, error handling, logging, and more—making your codebase cleaner and more maintainable.
Start small: implement an auth header interceptor, then add error handling or logging. As you become comfortable, explore advanced use cases like caching or request transformation. With interceptors, you’ll take full control of your app’s HTTP communication!