Functional HTTP Interceptors
HTTP interceptors sit between your application code and the network, letting you inspect and transform every request and response that flows through HttpClient. They are the idiomatic place to attach authentication tokens, log traffic, retry failures, or normalize errors — all without touching the services that actually call the API. Since Angular 15, interceptors can be written as plain functions and registered with withInterceptors, which is the modern, tree-shakable approach this page focuses on.
What a functional interceptor is
A functional interceptor is a function matching the HttpInterceptorFn type. It receives the outgoing HttpRequest and a next handler, and returns an Observable<HttpEvent>. You call next(req) to pass the request along the chain (eventually hitting the backend) and return the resulting stream — optionally transforming the request before, or the response after.
import { HttpInterceptorFn } from '@angular/common/http';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const started = Date.now();
console.log(`→ ${req.method} ${req.urlWithParams}`);
return next(req).pipe(
tap((event) => {
if (event.type === HttpEventType.Response) {
const ms = Date.now() - started;
console.log(`← ${event.status} ${req.url} (${ms}ms)`);
}
}),
);
};
Because requests are immutable, you never mutate req directly. Instead you produce a copy with req.clone({ ... }) and pass that to next.
Registering interceptors
Interceptors are wired up at bootstrap through provideHttpClient(withInterceptors([...])). The array order is the execution order: the first interceptor sees the request first and the response last.
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './app/interceptors/auth.interceptor';
import { loggingInterceptor } from './app/interceptors/logging.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, loggingInterceptor]),
),
],
});
Tip: Functional interceptors run inside Angular’s injection context, so you can call
inject()at the top of the function to grab services like a token store, router, or logger. This is what makes them so much cleaner than the old class-basedHTTP_INTERCEPTORSapproach.
Attaching an auth token
The classic use case is adding an Authorization header to outgoing requests. Inject the service that holds the token, clone the request with the header, and forward it.
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.token(); // a signal returning the current JWT
// Skip auth for public endpoints.
if (!token || req.url.includes('/public/')) {
return next(req);
}
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(authReq);
};
Transforming and handling responses
The same function can react to responses by piping the next(req) stream. A common pattern is catching 401 Unauthorized errors and redirecting to login, or surfacing a friendly message.
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((error: HttpErrorResponse) => {
if (error.status === 401) {
router.navigate(['/login']);
}
return throwError(() => error);
}),
);
};
Output:
→ GET /api/orders
← 401 /api/orders (38ms)
# navigation to /login triggered
Functional vs class-based interceptors
| Aspect | Functional (withInterceptors) | Class-based (HTTP_INTERCEPTORS) |
|---|---|---|
| Definition | Plain function (HttpInterceptorFn) | Class implementing HttpInterceptor |
| Registration | provideHttpClient(withInterceptors([...])) | Multi-provider with HTTP_INTERCEPTORS |
| Dependency injection | inject() inside the function | Constructor injection |
| Tree-shaking | Yes | Limited |
| Recommended | Yes (Angular 15+) | Legacy / interop only |
To keep DI-token-based interceptors working during a migration, add withInterceptorsFromDi() alongside withInterceptors().
Best Practices
- Keep each interceptor focused on one concern — auth, logging, retries — and compose them via the array rather than stuffing everything into one function.
- Always clone with
req.clone({ setHeaders: ... }); never mutate the original request. - Use
inject()at the top of the function and avoid heavy work on every request — interceptors run for all traffic. - Guard cross-cutting logic with URL or header checks so you don’t add tokens to third-party or public requests.
- Order matters: put request-shaping interceptors (auth) before observability ones (logging) so logs reflect the final request.
- Re-throw errors with
throwError(() => error)after handling so downstreamcatchErroroperators and component code still see them.