Almost every request needs the same extras: an auth header, error handling, a loading bar. Writing that in every service would be repetitive and easy to forget. An interceptor is a function that sees every request and response.
What you will learn
- How to write a functional interceptor
- How to add an auth header and handle errors in one place
- How to drive a global loading indicator
A loading counter service
// src/app/http/loading.ts
import { Injectable, computed, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class Loading {
private readonly pending = signal(0);
readonly active = computed(() => this.pending() > 0);
start(): void {
this.pending.update((n) => n + 1);
}
stop(): void {
this.pending.update((n) => Math.max(0, n - 1));
}
}
A counter, not a boolean, because several requests can run at the same time.
The interceptors
// src/app/http/interceptors.ts
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, finalize, throwError } from 'rxjs';
import { Auth } from '../auth/auth';
import { Loading } from './loading';
// 1. Add the token to requests that go to our API
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(Auth);
const name = auth.userName();
if (name && req.url.startsWith('https://api.example.com')) {
// Demo only. A real app sends a token received from the server.
req = req.clone({ setHeaders: { Authorization: `Bearer demo-token-for-${name}` } });
}
return next(req);
};
// 2. Turn the global loading flag on and off
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loading = inject(Loading);
loading.start();
return next(req).pipe(finalize(() => loading.stop()));
};
// 3. Handle errors in one place
export const errorInterceptor: HttpInterceptorFn = (req, next) =>
next(req).pipe(
catchError((error: unknown) => {
if (error instanceof HttpErrorResponse) {
if (error.status === 401) {
console.warn('Session expired. Redirect to login here.');
} else if (error.status >= 500) {
console.error('Server error', error.status, req.url);
}
}
// Re-throw so the caller can still show its own message.
return throwError(() => error);
}),
);
Requests are immutable. To change one, call req.clone({...}) and pass the clone to next. finalize runs on success, error and cancel, so the loading counter always goes back down.
Register them
// src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { APP_SETTINGS } from './di/app-config.token';
import { authInterceptor, errorInterceptor, loadingInterceptor } from './http/interceptors';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(
withFetch(),
// Order matters: the first interceptor runs first on the way out.
withInterceptors([loadingInterceptor, authInterceptor, errorInterceptor]),
),
{ provide: APP_SETTINGS, useValue: { apiUrl: 'https://api.example.com', featureBeta: true } },
],
};
Show the global loading bar
// src/app/app.ts
import { Component, inject } from '@angular/core';
import { Loading } from './http/loading';
@Component({
imports: [],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
protected readonly loading = inject(Loading);
}
<!-- src/app/app.html -->
@if (loading.active()) {
<div class="bar" role="progressbar" aria-label="Loading"></div>
}
<main>
<h1>Interceptors</h1>
</main>
/* src/app/app.css */
.bar { position: fixed; top: 0; left: 0; right: 0; height: 4px; background: #0b6cff; }
The key parts
- A functional interceptor has the signature
(req, next) => Observable.inject()works inside it. - Never mutate
req. Alwaysclone. - Add the token only to your own API, never to third-party URLs, or you may leak it.
- Interceptors also apply to
httpResource, because it usesHttpClient. - Keep interceptors small and single-purpose. Order them deliberately.
- Real token storage and refresh flows are security-sensitive, so see the security lesson.
Try it yourself
- Add a
retryinterceptor that retriesGETrequests once, but neverPOST. - Add a
X-Request-Idheader with a random id to every request. - Show a toast message for
status === 0(network down) in the error interceptor.
Quick check
- Why must you clone a request instead of changing it?
- Why use
finalizefor the loading counter? - Why only add the token to your own API URLs?
Next, organise bigger apps with a clear feature architecture. Read more in the interceptors guide.