Custom error types for validation failures and cache reservation conflicts, with patterns for graceful error handling in GitHub Actions workflows.
Error thrown for validation failures such as invalid cache keys, empty paths, or exceeded limits.
/**
* Error thrown for validation failures (key size, path validation, etc.)
*/
class ValidationError extends Error {
constructor(message: string);
readonly name: 'ValidationError';
}Common ValidationError scenarios:
Usage Examples:
import { restoreCache, saveCache, ValidationError } from '@actions/cache';
try {
// This will throw ValidationError: empty paths
await restoreCache([], 'my-key');
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation failed:', error.message);
process.exit(1); // Stop workflow on validation errors
}
}
try {
// This will throw ValidationError: key too long
const longKey = 'x'.repeat(600);
await saveCache(['node_modules'], longKey);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid cache key:', error.message);
// Fix the key and retry
}
}
try {
// This will throw ValidationError: too many keys
const primaryKey = 'main-key';
const restoreKeys = new Array(15).fill(0).map((_, i) => `key-${i}`);
await restoreCache(['dist'], primaryKey, restoreKeys);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Too many cache keys:', error.message);
// Reduce the number of restore keys
}
}Error thrown when unable to reserve cache, typically when another job is creating the same cache simultaneously.
/**
* Error thrown when unable to reserve cache (e.g., another job creating same cache)
*/
class ReserveCacheError extends Error {
constructor(message: string);
readonly name: 'ReserveCacheError';
}Common ReserveCacheError scenarios:
Usage Examples:
import { saveCache, ReserveCacheError } from '@actions/cache';
try {
const cacheId = await saveCache(['node_modules'], 'npm-deps-abc123');
console.log(`Cache saved with ID: ${cacheId}`);
} catch (error) {
if (error instanceof ReserveCacheError) {
console.warn('Cache reservation failed:', error.message);
console.warn('Another job may be creating this cache, continuing without caching');
// Continue workflow - this is not a fatal error
} else {
throw error; // Re-throw other errors
}
}
// Retry logic for ReserveCacheError
async function saveCacheWithRetry(paths: string[], key: string, maxRetries = 3): Promise<number | null> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await saveCache(paths, key);
} catch (error) {
if (error instanceof ReserveCacheError && attempt < maxRetries) {
console.warn(`Cache reservation attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
continue;
}
if (error instanceof ReserveCacheError) {
console.warn('Failed to reserve cache after all retries');
return null; // Give up gracefully
}
throw error; // Re-throw non-reservation errors
}
}
return null;
}Cache operations are designed to be non-blocking for workflow execution. Server errors and network issues are typically logged as warnings rather than failing the entire workflow.
// Error types that may be encountered
interface HttpClientError extends Error {
statusCode?: number;
}Error Handling Patterns:
import { restoreCache, saveCache, ValidationError, ReserveCacheError } from '@actions/cache';
import { HttpClientError } from '@actions/http-client';
async function robustCacheRestore(
paths: string[],
primaryKey: string,
restoreKeys?: string[]
): Promise<string | undefined> {
try {
return await restoreCache(paths, primaryKey, restoreKeys);
} catch (error) {
if (error instanceof ValidationError) {
// Validation errors should fail the workflow
console.error('Cache restore validation failed:', error.message);
throw error;
} else if (error instanceof HttpClientError && error.statusCode && error.statusCode >= 500) {
// Server errors are logged as errors but don't fail the workflow
console.error('Cache service error:', error.message);
return undefined;
} else {
// Other errors are warnings
console.warn('Cache restore failed:', error.message);
return undefined;
}
}
}
async function robustCacheSave(
paths: string[],
key: string
): Promise<number | null> {
try {
return await saveCache(paths, key);
} catch (error) {
if (error instanceof ValidationError) {
// Validation errors should fail the workflow
console.error('Cache save validation failed:', error.message);
throw error;
} else if (error instanceof ReserveCacheError) {
// Reservation errors are informational
console.info('Cache reservation failed (likely concurrent job):', error.message);
return null;
} else if (error instanceof HttpClientError && error.statusCode && error.statusCode >= 500) {
// Server errors are logged as errors but don't fail the workflow
console.error('Cache service error:', error.message);
return null;
} else {
// Other errors are warnings
console.warn('Cache save failed:', error.message);
return null;
}
}
}Here's a complete example showing proper error handling in a GitHub Actions workflow:
import {
restoreCache,
saveCache,
isFeatureAvailable,
ValidationError,
ReserveCacheError
} from '@actions/cache';
async function setupCacheWorkflow() {
// Check if cache service is available
if (!isFeatureAvailable()) {
console.log('Cache service not available, skipping cache operations');
return;
}
const paths = ['node_modules', 'packages/*/node_modules'];
const primaryKey = `npm-deps-${process.platform}-${hashFiles('**/package-lock.json')}`;
const restoreKeys = [`npm-deps-${process.platform}-`, 'npm-deps-'];
let cacheHit = false;
// Restore cache
try {
console.log('Attempting to restore cache...');
const cacheKey = await restoreCache(paths, primaryKey, restoreKeys);
if (cacheKey) {
console.log(`✅ Cache restored from key: ${cacheKey}`);
cacheHit = true;
} else {
console.log('❌ No cache found');
}
} catch (error) {
if (error instanceof ValidationError) {
console.error('❌ Cache restore validation failed:', error.message);
process.exit(1); // Fail workflow on validation errors
} else {
console.warn('⚠️ Cache restore failed:', error.message);
console.log('Continuing without cache...');
}
}
// Install dependencies if cache miss
if (!cacheHit) {
console.log('Installing dependencies...');
// await exec('npm ci');
}
// Save cache if it was a miss
if (!cacheHit) {
try {
console.log('Saving cache...');
const cacheId = await saveCache(paths, primaryKey);
console.log(`✅ Cache saved with ID: ${cacheId}`);
} catch (error) {
if (error instanceof ValidationError) {
console.error('❌ Cache save validation failed:', error.message);
process.exit(1); // Fail workflow on validation errors
} else if (error instanceof ReserveCacheError) {
console.info('ℹ️ Cache reservation failed:', error.message);
console.info('This usually means another job is creating the same cache');
} else {
console.warn('⚠️ Cache save failed:', error.message);
console.log('Build will continue without caching');
}
}
}
}
function hashFiles(pattern: string): string {
// Implementation to hash files matching pattern
// This would typically use a crypto hash of package-lock.json content
return 'abc123def456'; // Placeholder
}
// Run the workflow
setupCacheWorkflow().catch(error => {
console.error('Workflow failed:', error);
process.exit(1);
});Validate cache keys before using them:
function validateCacheKey(key: string): void {
if (key.length > 512) {
throw new ValidationError(`Cache key too long: ${key.length} characters (max 512)`);
}
if (key.includes(',')) {
throw new ValidationError(`Cache key cannot contain commas: ${key}`);
}
}
function createSafeCacheKey(base: string, hash: string): string {
const key = `${base}-${hash}`;
validateCacheKey(key);
return key;
}Ensure paths exist before caching:
import { existsSync } from 'fs';
function validatePaths(paths: string[]): void {
if (paths.length === 0) {
throw new ValidationError('At least one path is required');
}
const missingPaths = paths.filter(path => !existsSync(path));
if (missingPaths.length > 0) {
throw new ValidationError(`Paths do not exist: ${missingPaths.join(', ')}`);
}
}Always design cache operations to be optional:
async function installDependencies() {
let cacheHit = false;
// Try to restore cache, but don't fail if it doesn't work
try {
const cacheKey = await restoreCache(['node_modules'], 'npm-deps');
cacheHit = !!cacheKey;
} catch (error) {
console.warn('Cache restore failed, will install fresh dependencies');
}
// Install dependencies if no cache hit
if (!cacheHit) {
console.log('Installing dependencies...');
// await exec('npm ci');
// Try to save cache, but don't fail if it doesn't work
try {
await saveCache(['node_modules'], 'npm-deps');
} catch (error) {
console.warn('Cache save failed, build will continue');
}
}
}