HTTP Interceptors
An interceptor sits between your application code and the network, given the chance to inspect or modify every outgoing HttpRequest and every incoming HttpEvent before it reaches its final destination. This makes interceptors the natural place to implement cross-cutting HTTP concerns that would otherwise need to be duplicated in every service call: attaching an Authorization header, adding a correlation ID for tracing, logging request/response timing, transforming error responses into a consistent shape, or globally redirecting to a login page on a 401. Because interceptors compose into a chain, you can register several of them and each one decides whether to pass the request along unchanged, modify it, short-circuit it, or transform the response coming back.
Cricket analogy: An interceptor is like the third umpire reviewing every close call before it's finalized — checking DRS footage, confirming no-ball lines, adding a consistent replay overlay — so individual on-field umpires don't each have to independently re-derive the same review logic.
Writing a functional interceptor
Modern Angular (v15+) favors functional interceptors — plain functions matching the HttpInterceptorFn signature — over the older class-based HttpInterceptor interface, though both still work. A functional interceptor receives the outgoing HttpRequest and a next function representing the rest of the chain; calling next(req) forwards the request (optionally cloned with modifications, since HttpRequest instances are immutable) and returns an Observable<HttpEvent<unknown>> that you can further pipe with RxJS operators.
Cricket analogy: Functional interceptors (v15+) are like the modern DRS protocol replacing the old informal umpire-consultation system: a lightweight review function receives the delivery and a 'continue' signal representing the rest of the over, and can clone the replay with an added slow-motion angle before passing it along, since the original footage itself is unchangeable.
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (!token) {
return next(req);
}
const cloned = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(cloned);
};Registering interceptors with provideHttpClient
Functional interceptors are registered via the withInterceptors() feature function passed to provideHttpClient(), and they run in the exact array order you provide — the first interceptor sees the request first on the way out, and sees the response last on the way back, like layers of an onion. This ordering matters: an auth interceptor that adds a token should typically run before a logging interceptor that reads headers for debugging output.
Cricket analogy: withInterceptors() ordering is like a fielding chain of relay throws from boundary to keeper: the first fielder (auth interceptor) who gathers the ball and adds their throw-tag should act before the reader further down the chain (logging interceptor), and on the return throw, the last fielder to touch it is the first the batsman sees.
// app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
import { errorInterceptor } from './error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
],
};Global error handling in an interceptor
A dedicated error-handling interceptor is a common pattern for centralizing behavior like redirecting to a login page on a 401, showing a global toast notification on a 500, or normalizing varied backend error shapes into one consistent structure your components can rely on. Piping the next(req) call through catchError lets the interceptor react to any failed request across the entire application without every individual service needing its own error-handling boilerplate.
Cricket analogy: A dedicated error-handling interceptor is like a designated senior umpire who handles every controversial dismissal review centrally — redirecting a wrongly given out to a review, flagging a serious pitch-invasion incident to ground security, and normalizing every stoppage report into one consistent match report format.
import { HttpInterceptorFn, HttpErrorResponse } 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((err: HttpErrorResponse) => {
if (err.status === 401) {
router.navigate(['/login']);
}
return throwError(() => err);
})
);
};Interceptors are conceptually similar to Express/Koa middleware or Axios interceptors in the React/Vue ecosystems, but they're baked into Angular's DI system — a functional interceptor can call inject() to pull in any injectable service, giving it full access to application state like auth tokens or feature flags without manual wiring.
HttpRequest and HttpResponse objects are immutable by design, so you can never mutate req.headers directly — always use req.clone({...}) to produce a modified copy. Forgetting this and trying to assign to a request's properties directly will fail silently or be a no-op, and the original unmodified request will be sent.
- Interceptors sit between application code and the network, seeing every outgoing request and incoming response.
- Functional interceptors (HttpInterceptorFn) are the modern approach and can inject() services directly.
- Register interceptors with withInterceptors([...]) passed to provideHttpClient(); array order defines execution order.
- HttpRequest is immutable — use req.clone({...}) to attach headers or modify a request, never direct mutation.
- A dedicated error interceptor centralizes handling like redirecting on 401 or normalizing error shapes app-wide.
- Interceptors execute like nested middleware: the first in the array is outermost on both the request and response path.
Practice what you learned
1. What is the modern, recommended way to define an HTTP interceptor in Angular?
2. How do you register a list of functional interceptors in a standalone application?
3. Why must you call req.clone({...}) instead of mutating req.headers directly?
4. In provideHttpClient(withInterceptors([a, b])), which interceptor sees the outgoing request first?
5. What can a functional interceptor do that a plain RxJS operator chain in a service cannot easily achieve?
Was this page helpful?
You May Also Like
HttpClient and Fetching Data
Learn how Angular's HttpClient issues typed, Observable-based HTTP requests, and how to provide it in a standalone application.
RxJS Operators for HTTP (map, switchMap, catchError)
Master the core RxJS operators — map, switchMap, catchError, and their flattening cousins — used to transform, chain, and recover from HTTP requests.
Hierarchical Dependency Injection
Understand how Angular's injectors form a tree mirroring the component hierarchy, how token resolution walks up that tree, and how to scope service instances deliberately.
The inject() Function
Learn how the inject() function offers a flexible alternative to constructor injection, and how it enables DI in functional contexts like guards, resolvers, and interceptors.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics