HTTP Interceptors in Angular
HTTP interceptors sit between your application code and the actual HTTP transport. Every outgoing request and every incoming response passes through the interceptor chain, making interceptors the perfect place for cross-cutting concerns like authentication, logging, error handling, and caching.
Since Angular 15, the recommended approach is functional interceptors using HttpInterceptorFn.
How Interceptors Work
The interceptor chain works like middleware:
- Request leaves your service
- Each interceptor can inspect/modify the request
- Request reaches the server
- Response comes back through each interceptor in reverse order
- Your
subscribecallback receives the final response
Use Case | What the Interceptor Does |
|---|---|
Auth tokens | Adds Authorization header to every request |
Logging | Logs request URL and response time |
Error handling | Redirects to login on 401, shows toast on 500 |
Loading spinner | Increments/decrements a counter per request |
Caching | Returns cached response without hitting the server |
Request retries | Automatically retries failed requests |
Functional Interceptors (Angular 15+)
A functional interceptor is a plain function of type HttpInterceptorFn. Use inject() to access services.
// src/app/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (!token) {
return next(req); // no token — pass request through unchanged
}
// Clone the request and add the Authorization header
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`),
});
return next(authReq);
};req.clone() before modifying it — never mutate req directly.Registering Functional Interceptors
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor, // runs first (outermost)
loggingInterceptor,
errorInterceptor, // runs last before the server
])
),
],
};Logging Interceptor
// src/app/interceptors/logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { tap, finalize } from 'rxjs/operators';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const start = Date.now();
console.log(`--> ${req.method} ${req.url}`);
return next(req).pipe(
tap({
next: (event) => {
// tap into the response stream
},
error: (err) => {
console.error(`<-- ERROR ${req.url}:`, err.status, err.message);
},
}),
finalize(() => {
const elapsed = Date.now() - start;
console.log(`<-- ${req.method} ${req.url} [${elapsed}ms]`);
})
);
};Error Handling Interceptor
// src/app/interceptors/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 { NotificationService } from '../services/notification.service';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const notify = inject(NotificationService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Token expired — redirect to login
router.navigate(['/login']);
} else if (error.status === 403) {
notify.error('You do not have permission to do that.');
} else if (error.status === 404) {
notify.error('Resource not found.');
} else if (error.status >= 500) {
notify.error('Server error. Please try again later.');
}
return throwError(() => error);
})
);
};Loading Spinner Interceptor
// src/app/services/loading.service.ts
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoadingService {
private count = 0;
readonly isLoading = signal(false);
start(): void {
this.count++;
this.isLoading.set(true);
}
stop(): void {
this.count = Math.max(0, this.count - 1);
if (this.count === 0) this.isLoading.set(false);
}
}
// src/app/interceptors/loading.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs/operators';
import { LoadingService } from '../services/loading.service';
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loadingService = inject(LoadingService);
loadingService.start();
return next(req).pipe(
finalize(() => loadingService.stop())
);
};Token Refresh Interceptor
A common pattern is to catch a 401 response, silently refresh the access token, and replay the original request.
// src/app/interceptors/token-refresh.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';
export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status !== 401) {
return throwError(() => error);
}
// Attempt to refresh the token
return authService.refreshToken().pipe(
switchMap((newToken) => {
const retryReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${newToken}`),
});
return next(retryReq);
}),
catchError((refreshError) => {
authService.logout();
return throwError(() => refreshError);
})
);
})
);
};Caching Interceptor
// src/app/interceptors/cache.interceptor.ts
import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of, tap } from 'rxjs';
const cache = new Map<string, HttpResponse<unknown>>();
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Only cache GET requests
if (req.method !== 'GET') {
return next(req);
}
const cached = cache.get(req.url);
if (cached) {
console.log('Cache hit:', req.url);
return of(cached);
}
return next(req).pipe(
tap((event) => {
if (event instanceof HttpResponse) {
cache.set(req.url, event);
}
})
);
};Class-Based Interceptors (Legacy)
Before Angular 15 functional interceptors, class-based interceptors were the standard. They implement the HttpInterceptor interface.
// src/app/interceptors/auth-class.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
@Injectable()
export class AuthClassInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
if (!token) return next.handle(req);
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`),
});
return next.handle(authReq);
}
}
// Registration in AppModule:
// providers: [
// { provide: HTTP_INTERCEPTORS, useClass: AuthClassInterceptor, multi: true }
// ]Skipping Interceptors for Specific Requests
import { HttpContext, HttpContextToken } from '@angular/common/http';
// Define a context token
export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);
// In your interceptor
export const authInterceptor: HttpInterceptorFn = (req, next) => {
if (req.context.get(SKIP_AUTH)) {
return next(req); // skip this request
}
// ... add auth header
return next(req.clone({ headers: req.headers.set('Authorization', token) }));
};
// Usage — opt out per request
this.http.get('/api/public', {
context: new HttpContext().set(SKIP_AUTH, true),
})Best Practices
Prefer functional interceptors (HttpInterceptorFn) for new Angular 15+ projects
Always clone the request before modifying it — requests are immutable
Use HttpContext tokens to opt individual requests out of interceptors
Keep each interceptor focused on one concern (single responsibility)
Handle errors in a dedicated error interceptor, not scattered across services
Use finalize() for cleanup (e.g., stopping a spinner) — it runs on both success and error
Guard token refresh interceptors against infinite loops
Summary
HTTP interceptors are one of Angular's most powerful patterns for cross-cutting concerns. Functional interceptors introduced in Angular 15 make them simpler to write, test, and tree-shake. Common use cases include adding auth headers, logging, global error handling, loading spinners, and caching — all without touching individual service methods.