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.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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> }
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#
- authInterceptor clones the request with an extra header, only for the API origin.
- errorInterceptor watches responses with catchError and handles 401 and network errors centrally.
- Re-throwing the error lets individual components still show their own messages.
- loadingInterceptor counts active requests and decrements in finalize, even on errors.
- 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#
HttpRequest is immutable; req.headers.set(...) returns a new object that you ignored. Use req.clone(...).
Attaching your token to third-party URLs leaks it. Check the URL first.
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?
Show solution
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#
How do you add a header to an outgoing request in an interceptor?
Want more? Practise all Angular MCQs · Angular interview questions
My notes#
Saved in this browser.