We use cookies to enhance your experience on the site
CodeWorlds

Three Shields Against Failure - Errors and Cache

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.

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.

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.

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.

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.

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.

Go to CodeWorlds