Angular course ยท Module 9: HttpClient
Three Shields Against Failure - Errors and Cache
In this lesson3
A server behaves like weather on a battlefield: one moment the connection drops, the next the answer crawls in far too late. A samurai does not fall at the first stumble - he carries shields. In this lesson you will raise three of them: retry (try once more), catchError (turn a raw failure into something a human can read) and cache (never ask twice for the same thing). They come in that order, from reacting to a failure to avoiding the pointless work altogether.
Shield one: retry first, then translate the error
Many failures last only a moment - the network blinked and a second later it is back. Instead of surrendering on the first attempt, retry the request a few times, and turn only a stubborn failure into a readable message.
getSamurai(id: number): Observable<Samurai> {
return this.http.get<Samurai>('/api/samurai/' + id).pipe(
retry({ count: 3, delay: 1000 }), // 3 attempts, one second apart
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
let message = 'Unknown error!';
if (error.status === 0) message = 'No connection to the server';
else if (error.status === 404) message = 'Samurai not found';
else if (error.status >= 500) message = 'Server error - try again later';
else if (error.error?.message) message = error.error.message;
return throwError(() => new Error(message));
}Follow two shields at once here. retry({ count: 3, delay: 1000 }) repeats the request up to three times at one-second intervals - if the failure was momentary, the user never learns it happened. When it fails anyway, catchError hands the problem to handleError, which reads the status and returns a human message instead of a raw HttpErrorResponse. Look closely at status 0 - that is not a server reply at all but a dead connection, and it deserves its own branch because it means something entirely different from 500. Extend the same ladder with 403 for access denied when your API guards resources, and always close with throwError(() => new Error(message)), so the component still finds out that something went wrong.
Shield two: cache, or do not ask twice
If ten components ask for the same samurai, why send ten requests? A cache remembers the stream it fetched once and hands it to everyone who asks later.
export class CachedSamuraiService {
private cache = new Map<string, Observable<Samurai>>();
getSamurai(id: number): Observable<Samurai> {
const key = 'samurai-' + id;
if (!this.cache.has(key)) {
this.cache.set(key,
this.http.get<Samurai>('/api/samurai/' + id).pipe(
shareReplay({ bufferSize: 1, refCount: true })
)
);
}
return this.cache.get(key)!;
}
}The heart of it is shareReplay({ bufferSize: 1 }) - it makes the request fly once and replays the result to whoever subscribes afterwards. The cache map keeps the ready-made streams under a key, so a second getSamurai(5) never touches the server; it simply returns the remembered stream. Without shareReplay every subscriber would fire a fresh request - the most common trap in hand-rolled caching. Give the service a way out as well: an invalidate(id?) method that calls this.cache.delete(key) for one entry and this.cache.clear() for all of them, so that after a save you can force the next read to reach the server again.
Shield three: a cache that ages
Remembered data eventually goes stale. So you add a time to live (TTL): once it passes, the cache reaches for fresh data on its own.
export class TimedCacheService {
private cache = new Map<string, { data$: Observable<any>; expiry: number }>();
private readonly TTL = 5 * 60 * 1000; // 5 minutes
get<T>(url: string): Observable<T> {
const cached = this.cache.get(url);
if (cached && cached.expiry > Date.now()) {
return cached.data$; // still fresh - serve from cache
}
const data$ = this.http.get<T>(url).pipe(shareReplay({ bufferSize: 1 }));
this.cache.set(url, { data$, expiry: Date.now() + this.TTL });
return data$;
}
}The whole difference from the plain cache is one comparison: cached.expiry > Date.now(). If the entry is younger than five minutes you hand it over; if it is older you fetch again and store it with a fresh expiry stamp. That is a sensible compromise: you spare the server without showing an hour-old picture of the world. The right TTL depends on the data - a list of clans may happily live for an hour, the state of a battle only for seconds.
Remember three shields from this lesson: retry rescues you from a momentary outage, catchError translates a failure into a message, and cache (with shareReplay, optionally with a TTL) spares the server from repeating work it has already done. Hang them inside a service and one endpoint is safe; hang them on an interceptor and the whole castle is. Together they make the application as resilient as armour.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which RxJS operator do we use to handle HTTP errors?
2. What status does HttpErrorResponse have when the server could not be reached at all (network error)?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Complete the error handling in the service with logging and re-throw.
- Vertical ordering
Order the consecutive lines of an HTTP stream with retrying and error catching:
- Click in order
Build the call that re-emits an error from the stream: