Skip to content
Angular ng http 3 min read

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-based HTTP_INTERCEPTORS approach.

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

AspectFunctional (withInterceptors)Class-based (HTTP_INTERCEPTORS)
DefinitionPlain function (HttpInterceptorFn)Class implementing HttpInterceptor
RegistrationprovideHttpClient(withInterceptors([...]))Multi-provider with HTTP_INTERCEPTORS
Dependency injectioninject() inside the functionConstructor injection
Tree-shakingYesLimited
RecommendedYes (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 downstream catchError operators and component code still see them.
Last updated June 14, 2026
Was this helpful?