or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/cache-manager@7.2.x

docs

index.md
tile.json

tessl/npm-cache-manager

tessl install tessl/npm-cache-manager@7.2.0

Cache Manager for Node.js with support for multi-store caching, background refresh, and Keyv-compatible storage adapters

error-handling.mddocs/guides/

Error Handling Guide

Build robust caching with comprehensive error handling patterns.

Overview

cache-manager provides multiple error handling strategies:

  • Graceful degradation - Continue without cache on failures
  • Retry mechanisms - Automatic retry with backoff
  • Circuit breakers - Prevent cascading failures
  • Event monitoring - Track and alert on errors

Basic Error Handling

Try-Catch Pattern

try {
  await cache.set('key', 'value');
} catch (error) {
  console.error('Cache write failed:', error);
  // Continue without caching
}

try {
  const value = await cache.get('key');
} catch (error) {
  console.error('Cache read failed:', error);
  // Fetch from source directly
  return await fetchFromSource();
}

Wrap with Fallback

async function getCachedOrFetch<T>(
  key: string,
  fetchFn: () => Promise<T>,
  ttl: number
): Promise<T> {
  try {
    return await cache.wrap(key, fetchFn, ttl);
  } catch (error) {
    console.error(`Cache failed for ${key}, fetching directly:`, error);
    return await fetchFn();
  }
}

Event-Based Error Monitoring

Track All Errors

const errorMetrics = {
  get: 0,
  set: 0,
  del: 0,
  refresh: 0,
};

cache.on('get', ({ key, error }) => {
  if (error) {
    errorMetrics.get++;
    console.error(`Get error for ${key}:`, error);
  }
});

cache.on('set', ({ key, error }) => {
  if (error) {
    errorMetrics.set++;
    console.error(`Set error for ${key}:`, error);
  }
});

cache.on('refresh', ({ key, error }) => {
  if (error) {
    errorMetrics.refresh++;
    console.error(`Refresh error for ${key}:`, error);
  }
});

Error Rate Alerting

const errorWindow = {
  startTime: Date.now(),
  errorCount: 0,
  totalOps: 0,
};

function trackOperation(hasError: boolean) {
  errorWindow.totalOps++;
  if (hasError) errorWindow.errorCount++;
  
  const now = Date.now();
  if (now - errorWindow.startTime > 60000) { // Every minute
    const errorRate = errorWindow.totalOps > 0 
      ? (errorWindow.errorCount / errorWindow.totalOps) * 100 
      : 0;
    
    if (errorRate > 5) {
      console.error(`ALERT: Error rate ${errorRate.toFixed(2)}% exceeds threshold!`);
      // sendAlert(`Cache error rate: ${errorRate.toFixed(2)}%`);
    }
    
    // Reset window
    errorWindow.startTime = now;
    errorWindow.errorCount = 0;
    errorWindow.totalOps = 0;
  }
}

cache.on('get', ({ error }) => trackOperation(!!error));
cache.on('set', ({ error }) => trackOperation(!!error));

Retry Patterns

Exponential Backoff

async function setWithRetry<T>(
  key: string,
  value: T,
  ttl: number,
  maxRetries = 3
): Promise<void> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await cache.set(key, value, ttl);
      return; // Success
    } catch (error) {
      if (i === maxRetries - 1) throw error; // Last attempt failed
      
      const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
      console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Wrap with Retry

async function wrapWithRetry<T>(
  key: string,
  fetchFn: () => Promise<T>,
  ttl: number,
  maxRetries = 3
): Promise<T> {
  const retryFn = async (): Promise<T> => {
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await fetchFn();
      } catch (error) {
        if (i === maxRetries - 1) throw error;
        
        const delay = Math.pow(2, i) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
    throw new Error('Max retries exceeded');
  };
  
  return await cache.wrap(key, retryFn, ttl);
}

Circuit Breaker Pattern

Basic Circuit Breaker

class CircuitBreaker {
  private failures = 0;
  private readonly threshold = 5;
  private readonly timeout = 60000; // 1 minute
  private openUntil = 0;
  
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    const now = Date.now();
    
    // Check if circuit is open
    if (this.openUntil > now) {
      throw new Error('Circuit breaker is open');
    }
    
    try {
      const result = await fn();
      this.failures = 0; // Reset on success
      return result;
    } catch (error) {
      this.failures++;
      
      if (this.failures >= this.threshold) {
        this.openUntil = now + this.timeout;
        console.error('Circuit breaker opened');
      }
      
      throw error;
    }
  }
}

const breaker = new CircuitBreaker();

async function cachedFetch<T>(key: string, fetchFn: () => Promise<T>): Promise<T> {
  try {
    return await cache.wrap(
      key,
      () => breaker.execute(fetchFn),
      60000
    );
  } catch (error) {
    if (error.message === 'Circuit breaker is open') {
      // Return stale data if available
      const stale = await cache.get<T>(key);
      if (stale !== undefined) {
        console.warn('Circuit open, returning stale data');
        return stale;
      }
    }
    throw error;
  }
}

Advanced Circuit Breaker

class AdvancedCircuitBreaker {
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  private failures = 0;
  private successes = 0;
  private readonly failureThreshold = 5;
  private readonly successThreshold = 2;
  private readonly timeout = 60000;
  private openUntil = 0;
  
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    const now = Date.now();
    
    if (this.state === 'open') {
      if (this.openUntil <= now) {
        this.state = 'half-open';
        this.successes = 0;
      } else {
        throw new Error('Circuit breaker is open');
      }
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    
    if (this.state === 'half-open') {
      this.successes++;
      if (this.successes >= this.successThreshold) {
        this.state = 'closed';
        console.log('Circuit breaker closed');
      }
    }
  }
  
  private onFailure() {
    this.failures++;
    this.successes = 0;
    
    if (this.failures >= this.failureThreshold) {
      this.state = 'open';
      this.openUntil = Date.now() + this.timeout;
      console.error('Circuit breaker opened');
    }
  }
  
  getState() {
    return this.state;
  }
}

Stale-While-Revalidate

Return Stale Data on Error

async function staleWhileRevalidate<T>(
  key: string,
  fetchFn: () => Promise<T>,
  ttl: number
): Promise<T> {
  try {
    return await cache.wrap(key, fetchFn, ttl);
  } catch (error) {
    console.warn(`Fetch failed for ${key}, checking for stale data:`, error);
    
    // Try to return stale cached value
    const stale = await cache.get<T>(key);
    if (stale !== undefined) {
      console.log(`Returning stale data for ${key}`);
      
      // Trigger background refresh
      fetchFn().then(fresh => {
        cache.set(key, fresh, ttl).catch(console.error);
      });
      
      return stale;
    }
    
    throw error; // No stale data available
  }
}

Stale with Timeout

async function staleWithTimeout<T>(
  key: string,
  fetchFn: () => Promise<T>,
  ttl: number,
  timeout: number
): Promise<T> {
  const timeoutPromise = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), timeout);
  });
  
  try {
    return await Promise.race([
      cache.wrap(key, fetchFn, ttl),
      timeoutPromise
    ]);
  } catch (error) {
    if (error.message === 'Timeout') {
      // Return stale on timeout
      const stale = await cache.get<T>(key);
      if (stale !== undefined) {
        return stale;
      }
    }
    throw error;
  }
}

Store Connection Errors

Monitor Connection State

import { createCache } from 'cache-manager';
import { Keyv } from 'keyv';
import KeyvRedis from '@keyv/redis';

const redisStore = new KeyvRedis('redis://localhost:6379');
let isConnected = false;

redisStore.on('connect', () => {
  isConnected = true;
  console.log('Redis connected');
});

redisStore.on('error', (error) => {
  isConnected = false;
  console.error('Redis error:', error);
});

redisStore.on('disconnect', () => {
  isConnected = false;
  console.warn('Redis disconnected');
});

const cache = createCache({
  stores: [new Keyv({ store: redisStore })],
});

// Check connection before critical operations
async function safeCacheSet(key: string, value: any) {
  if (!isConnected) {
    console.warn('Cache unavailable, skipping');
    return;
  }
  
  await cache.set(key, value, 60000);
}

Graceful Degradation on Connection Loss

async function resilientGet<T>(key: string, fetchFn: () => Promise<T>): Promise<T> {
  try {
    const cached = await cache.get<T>(key);
    if (cached !== undefined) return cached;
  } catch (error) {
    console.error('Cache read failed, fetching directly:', error);
  }
  
  // Fetch from source
  const value = await fetchFn();
  
  // Try to cache (don't fail if cache unavailable)
  try {
    await cache.set(key, value, 60000);
  } catch (error) {
    console.error('Cache write failed (continuing):', error);
  }
  
  return value;
}

Multi-Store Error Handling

Partial Failure Tolerance

const cache = createCache({
  stores: [memoryStore, redisStore],
  nonBlocking: false, // Wait for all stores
});

cache.on('set', ({ key, store, error }) => {
  if (error) {
    console.error(`Failed to set ${key} in ${store}:`, error);
    // Operation continues, other stores may succeed
  }
});

// Even if Redis fails, memory cache may succeed
await cache.set('key', 'value');

Store-Specific Error Handling

const storeErrors = new Map<string, number>();

cache.on('set', ({ key, store, error }) => {
  if (error && store) {
    const count = storeErrors.get(store) || 0;
    storeErrors.set(store, count + 1);
    
    if (count > 10) {
      console.error(`ALERT: Store ${store} has ${count} consecutive errors`);
    }
  } else if (store) {
    storeErrors.delete(store); // Reset on success
  }
});

Serialization Errors

Handle Non-Serializable Values

async function safeSet(key: string, value: any, ttl: number): Promise<void> {
  try {
    // Test serialization
    JSON.stringify(value);
    
    await cache.set(key, value, ttl);
  } catch (error) {
    if (error.message.includes('circular') || error.message.includes('Converting circular structure')) {
      console.error(`Cannot cache ${key}: circular reference`);
    } else if (error.message.includes('not a function')) {
      console.error(`Cannot cache ${key}: contains non-serializable data`);
    } else {
      throw error;
    }
  }
}

Best Practices

1. Always Handle Cache Errors

// ❌ Don't let cache errors crash your app
await cache.get('key');

// ✅ Handle errors gracefully
try {
  await cache.get('key');
} catch (error) {
  console.error('Cache error:', error);
  // Continue with fallback
}

2. Use Events for Monitoring

// ✅ Monitor cache health
cache.on('get', ({ error }) => {
  if (error) metrics.errors++;
});

cache.on('set', ({ error }) => {
  if (error) metrics.errors++;
});

3. Fail Fast vs. Fail Silent

// Critical data: Fail fast
async function getCriticalData(key: string) {
  return await cache.get(key); // Throws on error
}

// Non-critical data: Fail silent
async function getNonCriticalData(key: string) {
  try {
    return await cache.get(key);
  } catch (error) {
    console.error('Cache error (continuing):', error);
    return undefined;
  }
}

4. Log with Context

cache.on('get', ({ key, error }) => {
  if (error) {
    console.error('Cache get failed', {
      key,
      error: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
    });
  }
});

Troubleshooting Common Errors

"Connection Refused"

Cause: Cache store (Redis) not running

Solution:

# Check if Redis is running
redis-cli ping

# Start Redis
redis-server

"Max Retries Exceeded"

Cause: Network issues or store overloaded

Solution:

  • Check network connectivity
  • Increase retry limit
  • Add circuit breaker

"Serialization Error"

Cause: Value contains non-serializable data (functions, circular refs)

Solution:

// Remove non-serializable properties before caching
const cacheable = {
  ...data,
  // Remove functions
  process: undefined,
  // Remove circular refs
  parent: undefined,
};
await cache.set('key', cacheable, 60000);

Next Steps

  • Quick Start - Basic error handling examples
  • Real-World Scenarios - Production error handling
  • Event System Reference - Complete error event documentation