Error Handling and Recovery
Learn comprehensive error handling strategies for robust Apache AGE applications using ageSchemaClient.
Understanding Error Types
Connection Errors
import { ConnectionError, DatabaseError } from 'age-schema-client';
try {
const client = new AgeSchemaClient({
connectionString: 'postgresql://user:pass@localhost:5432/graphdb',
graphName: 'my_graph'
});
await client.connect();
} catch (error) {
if (error instanceof ConnectionError) {
console.error('Failed to connect to database:', error.message);
// Handle connection issues
await handleConnectionError(error);
} else {
console.error('Unexpected error:', error);
}
}
Query Errors
import { QueryError, SchemaError } from 'age-schema-client';
try {
const result = await client.query()
.match('(p:Person)')
.where({ name: 'Alice' })
.return('p')
.execute();
} catch (error) {
if (error instanceof QueryError) {
console.error('Query failed:', error.message);
console.error('Query:', error.query);
console.error('Parameters:', error.parameters);
} else if (error instanceof SchemaError) {
console.error('Schema validation failed:', error.message);
console.error('Validation errors:', error.errors);
}
}
Batch Loading Errors
import { BatchLoaderError } from 'age-schema-client';
try {
const batchLoader = client.createBatchLoader();
await batchLoader.load(graphData);
} catch (error) {
if (error instanceof BatchLoaderError) {
console.error('Batch loading failed:', error.message);
console.error('Failed items:', error.failedItems);
console.error('Context:', error.context);
// Retry failed items individually
await retryFailedItems(error.failedItems);
}
}
Retry Strategies
Exponential Backoff
class RetryManager {
async executeWithRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
throw lastError;
}
// Check if error is retryable
if (!this.isRetryableError(error)) {
throw error;
}
// Calculate delay with exponential backoff
const delay = baseDelay * Math.pow(2, attempt);
const jitter = Math.random() * 0.1 * delay; // Add 10% jitter
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay + jitter}ms...`);
await this.sleep(delay + jitter);
}
}
throw lastError!;
}
private isRetryableError(error: any): boolean {
// Connection timeouts, temporary network issues
if (error instanceof ConnectionError) {
return true;
}
// Database connection issues
if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
return true;
}
// PostgreSQL specific retryable errors
const retryableCodes = [
'53300', // too_many_connections
'53400', // configuration_limit_exceeded
'08006', // connection_failure
'08001', // sqlclient_unable_to_establish_sqlconnection
];
return retryableCodes.includes(error.code);
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const retryManager = new RetryManager();
const result = await retryManager.executeWithRetry(
() => client.query()
.match('(p:Person)')
.return('p')
.execute(),
3, // max retries
1000 // base delay
);
Circuit Breaker Pattern
class CircuitBreaker {
private failures = 0;
private lastFailureTime = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private failureThreshold: number = 5,
private recoveryTimeout: number = 60000 // 1 minute
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
}
}
getState() {
return {
state: this.state,
failures: this.failures,
lastFailureTime: this.lastFailureTime
};
}
}
// Usage
const circuitBreaker = new CircuitBreaker(5, 60000);
try {
const result = await circuitBreaker.execute(() =>
client.query()
.match('(p:Person)')
.return('p')
.execute()
);
} catch (error) {
console.error('Operation failed or circuit breaker is open:', error.message);
}
Graceful Degradation
Fallback Strategies
class GraphService {
constructor(
private client: AgeSchemaClient,
private cache: Map<string, any> = new Map()
) {}
async getPersonWithFallback(name: string): Promise<any> {
try {
// Primary: Try graph database
return await this.getPersonFromGraph(name);
} catch (error) {
console.warn('Graph query failed, trying cache:', error.message);
try {
// Fallback 1: Try cache
const cached = this.cache.get(`person:${name}`);
if (cached) {
return cached;
}
// Fallback 2: Return minimal data
return {
name,
source: 'fallback',
message: 'Limited data available due to system issues'
};
} catch (fallbackError) {
console.error('All fallbacks failed:', fallbackError);
throw new Error('Service temporarily unavailable');
}
}
}
private async getPersonFromGraph(name: string): Promise<any> {
const result = await this.client.query()
.match('(p:Person)')
.where({ name })
.return('p')
.execute();
if (result.length > 0) {
// Cache successful results
this.cache.set(`person:${name}`, result[0]);
return result[0];
}
throw new Error('Person not found');
}
}
Partial Failure Handling
async function loadDataWithPartialFailures(datasets: any[][]) {
const results = {
successful: 0,
failed: 0,
errors: [] as any[]
};
// Process each dataset independently
const promises = datasets.map(async (dataset, index) => {
try {
const batchLoader = client.createBatchLoader();
await batchLoader.load({
vertices: dataset,
edges: []
});
results.successful++;
console.log(`Dataset ${index} loaded successfully`);
} catch (error) {
results.failed++;
results.errors.push({
dataset: index,
error: error.message,
itemCount: dataset.length
});
console.error(`Dataset ${index} failed:`, error.message);
}
});
await Promise.allSettled(promises);
console.log('Loading summary:', results);
// Decide whether to continue based on success rate
const successRate = results.successful / (results.successful + results.failed);
if (successRate < 0.5) {
throw new Error(`Too many failures: ${results.failed}/${datasets.length} datasets failed`);
}
return results;
}
Validation and Data Integrity
Input Validation
function validateGraphData(data: any): void {
const errors: string[] = [];
// Validate vertices
if (data.vertices) {
data.vertices.forEach((vertex: any, index: number) => {
if (!vertex.label) {
errors.push(`Vertex ${index}: Missing label`);
}
if (!vertex.properties || typeof vertex.properties !== 'object') {
errors.push(`Vertex ${index}: Invalid properties`);
}
if (vertex.properties && !vertex.properties.id) {
errors.push(`Vertex ${index}: Missing required 'id' property`);
}
});
}
// Validate edges
if (data.edges) {
data.edges.forEach((edge: any, index: number) => {
if (!edge.label) {
errors.push(`Edge ${index}: Missing label`);
}
if (!edge.from || !edge.to) {
errors.push(`Edge ${index}: Missing from/to specification`);
}
if (edge.from && (!edge.from.label || !edge.from.properties)) {
errors.push(`Edge ${index}: Invalid 'from' specification`);
}
if (edge.to && (!edge.to.label || !edge.to.properties)) {
errors.push(`Edge ${index}: Invalid 'to' specification`);
}
});
}
if (errors.length > 0) {
throw new Error(`Validation failed:\n${errors.join('\n')}`);
}
}
// Usage
try {
validateGraphData(graphData);
await client.createBatchLoader().load(graphData);
} catch (error) {
console.error('Data validation failed:', error.message);
// Handle validation errors appropriately
}
Transaction Rollback
async function safeDataOperation() {
try {
await client.transaction(async (tx) => {
// Multiple operations within transaction
await tx.query()
.create('(p:Person {name: "Alice", age: 30})')
.execute();
await tx.query()
.create('(p:Person {name: "Bob", age: 25})')
.execute();
// This might fail
await tx.query()
.match('(a:Person), (b:Person)')
.where({ 'a.name': 'Alice', 'b.name': 'Bob' })
.create('(a)-[r:KNOWS]->(b)')
.execute();
console.log('All operations completed successfully');
});
} catch (error) {
console.error('Transaction failed and was rolled back:', error.message);
// All changes are automatically rolled back
}
}
Monitoring and Alerting
Error Tracking
class ErrorTracker {
private errors: Map<string, number> = new Map();
private errorDetails: any[] = [];
track(error: Error, context?: any) {
const errorKey = `${error.constructor.name}:${error.message}`;
const count = this.errors.get(errorKey) || 0;
this.errors.set(errorKey, count + 1);
this.errorDetails.push({
timestamp: new Date().toISOString(),
type: error.constructor.name,
message: error.message,
stack: error.stack,
context
});
// Keep only last 1000 errors
if (this.errorDetails.length > 1000) {
this.errorDetails = this.errorDetails.slice(-1000);
}
// Alert on high error rates
if (count > 10) {
this.alert(`High error rate detected: ${errorKey} occurred ${count} times`);
}
}
getStats() {
return {
errorCounts: Object.fromEntries(this.errors),
recentErrors: this.errorDetails.slice(-10),
totalErrors: this.errorDetails.length
};
}
private alert(message: string) {
console.error('ALERT:', message);
// Send to monitoring system, email, etc.
}
}
// Global error tracker
const errorTracker = new ErrorTracker();
// Wrap operations with error tracking
async function trackedOperation<T>(
operation: () => Promise<T>,
context?: any
): Promise<T> {
try {
return await operation();
} catch (error) {
errorTracker.track(error as Error, context);
throw error;
}
}
// Usage
try {
await trackedOperation(
() => client.query().match('(p:Person)').return('p').execute(),
{ operation: 'get_all_persons' }
);
} catch (error) {
// Error is already tracked
console.error('Operation failed:', error.message);
}
Health Checks
class HealthChecker {
constructor(private client: AgeSchemaClient) {}
async checkHealth(): Promise<{
status: 'healthy' | 'degraded' | 'unhealthy';
checks: any[];
}> {
const checks = [];
let overallStatus: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
// Check database connection
try {
await this.client.query().raw('SELECT 1').execute();
checks.push({ name: 'database_connection', status: 'healthy' });
} catch (error) {
checks.push({
name: 'database_connection',
status: 'unhealthy',
error: error.message
});
overallStatus = 'unhealthy';
}
// Check graph accessibility
try {
await this.client.query()
.raw('SELECT * FROM ag_catalog.ag_graph LIMIT 1')
.execute();
checks.push({ name: 'graph_access', status: 'healthy' });
} catch (error) {
checks.push({
name: 'graph_access',
status: 'degraded',
error: error.message
});
if (overallStatus === 'healthy') {
overallStatus = 'degraded';
}
}
// Check connection pool
const pool = this.client.getPool();
const poolHealth = pool.totalCount > 0 && pool.idleCount >= 0;
checks.push({
name: 'connection_pool',
status: poolHealth ? 'healthy' : 'degraded',
details: {
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount
}
});
return { status: overallStatus, checks };
}
}
// Usage
const healthChecker = new HealthChecker(client);
setInterval(async () => {
try {
const health = await healthChecker.checkHealth();
console.log('Health check:', health);
if (health.status === 'unhealthy') {
// Trigger alerts, restart services, etc.
console.error('System is unhealthy!');
}
} catch (error) {
console.error('Health check failed:', error);
}
}, 30000); // Every 30 seconds
Best Practices
- Always wrap operations in try-catch blocks
- Use specific error types for different failure scenarios
- Implement retry logic for transient failures
- Use circuit breakers for external dependencies
- Validate input data before processing
- Use transactions for multi-step operations
- Monitor error rates and patterns
- Implement graceful degradation strategies
- Log errors with context for debugging
- Test error scenarios in your application
Next Steps
- Performance Optimization - Optimize for better reliability
- Troubleshooting - Common issues and solutions
- Bulk Loading - Robust data loading strategies