Error Handling and Logging Guide
This document describes the error handling and logging patterns used in the Eryxon MES application.
Table of Contents
Section titled “Table of Contents”Error Handling Utilities
Section titled “Error Handling Utilities”Location: src/lib/errors.ts
Error Codes
Section titled “Error Codes”Use predefined error codes for consistency:
import { ErrorCode } from '@/lib/errors';
// Available codes:ErrorCode.UNAUTHORIZED // User not authenticatedErrorCode.FORBIDDEN // User lacks permissionErrorCode.NOT_FOUND // Resource not foundErrorCode.VALIDATION_ERROR // Invalid inputErrorCode.QUOTA_EXCEEDED // Storage/usage limit reachedErrorCode.NETWORK_ERROR // Network connectivity issueErrorCode.TIMEOUT // Operation timed out// ... and moreAppError Class
Section titled “AppError Class”Create rich error objects with context:
import { AppError, ErrorCode } from '@/lib/errors';
throw new AppError('Job not found', ErrorCode.NOT_FOUND, { context: { jobId: '123', tenantId: 'abc' }, httpStatus: 404, isRetryable: false,});Safe Error Message Extraction
Section titled “Safe Error Message Extraction”Always use getErrorMessage for unknown error types:
import { getErrorMessage } from '@/lib/errors';
try { await someOperation();} catch (error) { const message = getErrorMessage(error); // Always returns string toast.error(message);}Supabase Error Conversion
Section titled “Supabase Error Conversion”Convert Supabase errors to AppError for consistent handling:
import { fromSupabaseError } from '@/lib/errors';
const { data, error } = await supabase.from('jobs').select('*');if (error) { throw fromSupabaseError(error, { operation: 'fetchJobs' });}Result Type Pattern
Section titled “Result Type Pattern”For operations where failure is expected, use the Result type:
import { Result, ok, err, tryCatch } from '@/lib/errors';
async function fetchJob(id: string): Promise<Result<Job>> { const result = await tryCatch( supabase.from('jobs').select('*').eq('id', id).single(), { operation: 'fetchJob' } );
if (!result.success) { return result; // Returns { success: false, error: AppError } }
return ok(result.data); // Returns { success: true, data: Job }}
// Usage:const result = await fetchJob('123');if (result.success) { console.log(result.data);} else { console.error(result.error.toUserMessage());}Logging System
Section titled “Logging System”Location: src/lib/logger.ts
Basic Usage
Section titled “Basic Usage”import { logger } from '@/lib/logger';
// Log levelslogger.debug('Detailed info for development');logger.info('General operational information');logger.warn('Potential issues');logger.error('Errors that affect functionality', error);Logging with Context
Section titled “Logging with Context”Always include context for better debugging:
logger.info('Job created', { operation: 'createJob', entityType: 'job', entityId: jobId, tenantId: profile.tenant_id,});
logger.error('Failed to update operation', error, { operation: 'updateOperation', entityType: 'operation', entityId: operationId,});Supabase Operation Logging
Section titled “Supabase Operation Logging”Log database operations consistently:
const { data, error } = await supabase.from('jobs').insert(newJob);
logger.supabaseOperation('INSERT', 'jobs', { data, error }, { entityId: newJob.id, tenantId: tenantId,});Timed Operations
Section titled “Timed Operations”Track operation performance:
// Synchronousconst result = logger.timed('processData', () => { return heavyComputation();});
// Asynchronousconst data = await logger.timedAsync('fetchMetrics', async () => { return await fetchAllMetrics();}, { tenantId });Scoped Loggers
Section titled “Scoped Loggers”Create loggers with preset context for components/hooks:
import { createScopedLogger } from '@/lib/logger';
function useJobOperations(jobId: string) { const log = createScopedLogger({ operation: 'useJobOperations', entityType: 'job', entityId: jobId, });
const updateJob = async (data: Partial<Job>) => { log.info('Updating job'); try { await supabase.from('jobs').update(data).eq('id', jobId); log.info('Job updated successfully'); } catch (error) { log.error('Failed to update job', error); throw error; } };}Log Levels by Environment
Section titled “Log Levels by Environment”- Development: All logs (debug, info, warn, error)
- Production: Only warn and error
Error Boundary
Section titled “Error Boundary”Location: src/components/ErrorBoundary.tsx
Basic Usage
Section titled “Basic Usage”Wrap components that might throw:
import { ErrorBoundary } from '@/components/ErrorBoundary';
<ErrorBoundary> <ComponentThatMightFail /></ErrorBoundary>Custom Fallback
Section titled “Custom Fallback”Provide a custom error UI:
<ErrorBoundary fallback={<CustomErrorComponent />} onError={(error, info) => { // Send to error tracking service trackError(error, info); }}> <MyComponent /></ErrorBoundary>HOC Pattern
Section titled “HOC Pattern”Wrap components with error boundary:
import { withErrorBoundary } from '@/components/ErrorBoundary';
const SafeComponent = withErrorBoundary(MyComponent, { errorMessage: 'Failed to load component',});Lazy Loading Fallback
Section titled “Lazy Loading Fallback”Use with React.lazy for code splitting:
import { PageLoadingFallback } from '@/components/ErrorBoundary';
<Suspense fallback={<PageLoadingFallback />}> <LazyComponent /></Suspense>Best Practices
Section titled “Best Practices”1. Always Handle Errors
Section titled “1. Always Handle Errors”Never leave catch blocks empty:
// BADtry { await operation();} catch (error) { // Silent failure}
// GOODtry { await operation();} catch (error) { logger.error('Operation failed', error, { operation: 'myOperation' }); toast.error(getErrorMessage(error));}2. Use Typed Errors
Section titled “2. Use Typed Errors”Avoid any in catch blocks:
// BADcatch (error: any) { console.error(error.message);}
// GOODcatch (error) { const message = getErrorMessage(error); logger.error('Operation failed', error);}3. Include Context
Section titled “3. Include Context”Always log with relevant context:
// BADlogger.error('Error');
// GOODlogger.error('Failed to create job', error, { operation: 'createJob', tenantId, userId, jobData: { name: job.name },});4. User-Friendly Messages
Section titled “4. User-Friendly Messages”Convert technical errors for users:
const appError = fromSupabaseError(error);toast.error(appError.toUserMessage()); // "The requested resource was not found."5. Retry Logic for Transient Errors
Section titled “5. Retry Logic for Transient Errors”Check if error is retryable:
import { isRetryableError } from '@/lib/errors';
const MAX_RETRIES = 3;let attempt = 0;
while (attempt < MAX_RETRIES) { try { await operation(); break; } catch (error) { if (!isRetryableError(error) || attempt === MAX_RETRIES - 1) { throw error; } attempt++; await delay(Math.pow(2, attempt) * 1000); // Exponential backoff }}Migration Guide
Section titled “Migration Guide”Converting Existing Code
Section titled “Converting Existing Code”Before (Old Pattern)
Section titled “Before (Old Pattern)”try { const { data, error } = await supabase.from('jobs').select('*'); if (error) throw error; return data;} catch (error) { console.error('Error fetching jobs:', error); throw error;}After (New Pattern)
Section titled “After (New Pattern)”import { logger } from '@/lib/logger';import { fromSupabaseError, getErrorMessage } from '@/lib/errors';
try { const { data, error } = await supabase.from('jobs').select('*'); if (error) { throw fromSupabaseError(error, { operation: 'fetchJobs' }); } return data;} catch (error) { logger.error('Failed to fetch jobs', error, { operation: 'fetchJobs', tenantId, }); throw error;}Priority Areas for Migration
Section titled “Priority Areas for Migration”- Authentication flows - Critical for security
- Data mutations - Important for data integrity
- External integrations - Webhooks, MQTT, APIs
- File operations - Upload/download errors
Related Documentation
Section titled “Related Documentation”- CODING_PATTERNS.md - General coding patterns
- CACHING.md - Query caching patterns
- CLAUDE.md - AI agent guidelines