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.
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.
1getSamurai(id: number): Observable<Samurai> {
2 return this.http.get<Samurai>('/api/samurai/' + id).pipe(
3 retry({ count: 3, delay: 1000 }), // 3 attempts, one second apart
4 catchError(this.handleError)
5 );
6}
7
8private handleError(error: HttpErrorResponse): Observable<never> {
9 let message = 'Unknown error!';
10 if (error.status === 0) message = 'No connection to the server';
11 else if (error.status === 404) message = 'Samurai not found';
12 else if (error.status >= 500) message = 'Server error - try again later';
13 else if (error.error?.message) message = error.error.message;
14
15 return throwError(() => new Error(message));
16}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.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.
1export class CachedSamuraiService {
2 private cache = new Map<string, Observable<Samurai>>();
3
4 getSamurai(id: number): Observable<Samurai> {
5 const key = 'samurai-' + id;
6
7 if (!this.cache.has(key)) {
8 this.cache.set(key,
9 this.http.get<Samurai>('/api/samurai/' + id).pipe(
10 shareReplay({ bufferSize: 1, refCount: true })
11 )
12 );
13 }
14 return this.cache.get(key)!;
15 }
16}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.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.
1export class TimedCacheService {
2 private cache = new Map<string, { data$: Observable<any>; expiry: number }>();
3 private readonly TTL = 5 * 60 * 1000; // 5 minutes
4
5 get<T>(url: string): Observable<T> {
6 const cached = this.cache.get(url);
7 if (cached && cached.expiry > Date.now()) {
8 return cached.data$; // still fresh - serve from cache
9 }
10
11 const data$ = this.http.get<T>(url).pipe(shareReplay({ bufferSize: 1 }));
12 this.cache.set(url, { data$, expiry: Date.now() + this.TTL });
13 return data$;
14 }
15}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.