tessl install tessl/npm-cache-manager@7.2.0Cache Manager for Node.js with support for multi-store caching, background refresh, and Keyv-compatible storage adapters
Build robust caching with comprehensive error handling patterns.
cache-manager provides multiple error handling strategies:
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();
}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();
}
}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);
}
});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));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));
}
}
}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);
}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;
}
}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;
}
}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
}
}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;
}
}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);
}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;
}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');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
}
});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;
}
}
}// ❌ 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
}// ✅ Monitor cache health
cache.on('get', ({ error }) => {
if (error) metrics.errors++;
});
cache.on('set', ({ error }) => {
if (error) metrics.errors++;
});// 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;
}
}cache.on('get', ({ key, error }) => {
if (error) {
console.error('Cache get failed', {
key,
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
});
}
});Cause: Cache store (Redis) not running
Solution:
# Check if Redis is running
redis-cli ping
# Start Redis
redis-serverCause: Network issues or store overloaded
Solution:
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);