Angular · Architecture

Interceptors

An interceptor sees every HTTP request and response in your app. Write the logic once — add the token, catch 401s, count requests in flight — instead of repeating it in every service.

Advanced 6 min Lesson 30 of 38Examples target Angular 20 with standalone components. Each lesson states the version it was verified against.
Before you start:HttpClient
0/38 in Angular
On this page · 6 min · Advanced

Learning objectives#

  • Write a functional interceptor
  • Add headers by cloning the request
  • Handle errors such as 401 globally
  • Track loading state for all requests

Explanation#

An HttpInterceptorFn receives (req, next). Return next(req) to pass the request on; you can change the request before and process the response after.

Requests are immutable: req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) creates a modified copy.

Use RxJS on the response: catchError to react to 401 (log out, go to login), finalize to run code when the request ends.

Register interceptors in app.config.ts: provideHttpClient(withInterceptors([authInterceptor, errorInterceptor, loadingInterceptor])). They run in that order for requests and in reverse for responses.

Only add tokens to your own API (check the URL), never to third-party domains.

Syntax#

TypeScript Illustrative
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};


provideHttpClient(withInterceptors([authInterceptor]))

Code example: Auth, error and loading interceptors#

TypeScriptinterceptors.ts Illustrative
import { ApplicationConfig, Injectable, computed, inject, signal } from '@angular/core';
import { HttpErrorResponse, HttpInterceptorFn, provideHttpClient, withInterceptors } from '@angular/common/http';
import { Router, provideRouter } from '@angular/router';
import { catchError, finalize, throwError } from 'rxjs';


const API_ORIGIN = 'https://api.shop.example.com';


@Injectable({ providedIn: 'root' })
export class AuthStore {
  readonly token = signal<string | null>('demo-token');
  logout() { this.token.set(null); }
}


@Injectable({ providedIn: 'root' })
export class LoadingStore {
  private active = signal(0);
  readonly busy = computed(() => this.active() > 0);
  start() { this.active.update(n => n + 1); }
  stop() { this.active.update(n => Math.max(0, n - 1)); }
}


// 1. Add the token — only for our API
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthStore).token();
  if (!token || !req.url.startsWith(API_ORIGIN)) return next(req);
  return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};


// 2. Handle errors in one place
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthStore);
  const router = inject(Router);
  return next(req).pipe(
    catchError((err: HttpErrorResponse) => {
      if (err.status === 401) {
        auth.logout();
        router.navigate(['/login'], { queryParams: { expired: 1 } });
      } else if (err.status === 0) {
        console.error('Network problem — is the API reachable?');
      }
      return throwError(() => err);              // let callers still react
    }),
  );
};


// 3. Global loading indicator
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  const loading = inject(LoadingStore);
  loading.start();
  return next(req).pipe(finalize(() => loading.stop()));
};


export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([]),
    provideHttpClient(withInterceptors([authInterceptor, errorInterceptor, loadingInterceptor])),
  ],
};
// In the shell template: @if (loading.busy()) { <div class="progress-bar"></div> }
Expected output
Every request to https://api.shop.example.com automatically carries “Authorization: Bearer demo-token”; requests to other domains do not. While any request is running, LoadingStore.busy() is true (show a progress bar). A 401 from any endpoint logs the user out and navigates to /login?expired=1; the original caller still receives the error.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. authInterceptor clones the request with an extra header, only for the API origin.
  2. errorInterceptor watches responses with catchError and handles 401 and network errors centrally.
  3. Re-throwing the error lets individual components still show their own messages.
  4. loadingInterceptor counts active requests and decrements in finalize, even on errors.
  5. withInterceptors registers all three in order.

Real-world use case#

A banking app’s interceptors attach the session token, add an X-Correlation-Id header for support tracing, show one global progress bar, and send users to the login page when the session expires — with zero changes to the 40 API services.

Common mistakes#

Mutating the request

HttpRequest is immutable; req.headers.set(...) returns a new object that you ignored. Use req.clone(...).

Sending tokens to every domain

Attaching your token to third-party URLs leaks it. Check the URL first.

Swallowing errors

Returning EMPTY or of(null) for every error hides failures from callers. Re-throw after global handling.

Best practices#

  • Functional interceptors + withInterceptors
  • Clone requests to change them
  • Handle 401 and network errors centrally, then re-throw
  • Keep each interceptor focused on one job

Remember this#

Interceptor = (req, next) => next(modifiedReq).

req.clone({ setHeaders: { … } }).

Requests in order, responses in reverse.

Practice exercise#

Your task: Write a correlationIdInterceptor that adds an X-Correlation-Id header with crypto.randomUUID() to every request.

Need a hint?
return next(req.clone({ setHeaders: { "X-Correlation-Id": crypto.randomUUID() } }));
Show solution
TypeScript Illustrative
import { HttpInterceptorFn } from '@angular/common/http';


export const correlationIdInterceptor: HttpInterceptorFn = (req, next) =>
  next(req.clone({ setHeaders: { 'X-Correlation-Id': crypto.randomUUID() } }));
// provideHttpClient(withInterceptors([correlationIdInterceptor, authInterceptor]))

Mini quiz#

Interceptors quizMultiple choice Question 1 of 3

How do you add a header to an outgoing request in an interceptor?

Score so far: 0/0

Want more? Practise all Angular MCQs · Angular interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.