Awesome-omni-skill error-handling
Comprehensive error handling patterns for Splits Network services and apps
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/error-handling" ~/.claude/skills/diegosouzapw-awesome-omni-skill-error-handling && rm -rf "$T"
skills/development/error-handling/SKILL.mdError Handling Skill
This skill provides guidance for consistent, user-friendly error handling across Splits Network.
Purpose
Help developers implement robust error handling:
- HTTP Status Codes: Correct status codes for API responses
- Error Response Format: Standardized error structure
- Error Classes: Custom error types for different scenarios
- Frontend Error Handling: User-friendly error messages
- Logging: Error logging best practices
When to Use This Skill
Use this skill when:
- Implementing API error responses
- Creating custom error classes
- Handling errors in frontend components
- Logging errors for debugging
- Displaying error messages to users
Core Principles
1. HTTP Status Codes
Use correct HTTP status codes for API responses:
// 400 Bad Request - Client error (validation, malformed request) if (!isValidEmail(email)) { return reply.code(400).send({ error: { code: 'VALIDATION_ERROR', message: 'Invalid email format', details: { field: 'email' } } }); } // 401 Unauthorized - Missing or invalid authentication if (!request.headers['x-clerk-user-id']) { return reply.code(401).send({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } }); } // 403 Forbidden - Valid auth but insufficient permissions if (!canAccessResource(userId, resourceId)) { return reply.code(403).send({ error: { code: 'FORBIDDEN', message: 'You do not have permission to access this resource' } }); } // 404 Not Found - Resource doesn't exist const job = await repository.getById(id); if (!job) { return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Job not found' } }); } // 409 Conflict - Resource state conflict const existing = await repository.findByEmail(email); if (existing) { return reply.code(409).send({ error: { code: 'CONFLICT', message: 'User with this email already exists' } }); } // 422 Unprocessable Entity - Semantic validation error if (application.stage === 'closed') { return reply.code(422).send({ error: { code: 'INVALID_STATE', message: 'Cannot update closed application' } }); } // 429 Too Many Requests - Rate limit exceeded if (rateLimitExceeded) { return reply.code(429).send({ error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests, please try again later', retryAfter: 60 } }); } // 500 Internal Server Error - Unexpected server error catch (error) { console.error('Unexpected error:', error); return reply.code(500).send({ error: { code: 'INTERNAL_SERVER_ERROR', message: 'An unexpected error occurred' } }); } // 503 Service Unavailable - External dependency failure if (!canConnectToDatabase) { return reply.code(503).send({ error: { code: 'SERVICE_UNAVAILABLE', message: 'Database unavailable, please try again later' } }); }
See references/http-status-codes.md.
2. Error Response Format
All error responses follow standard envelope:
{ "error": { "code": "ERROR_CODE", // Machine-readable error code "message": "User-friendly message", // Human-readable description "details"?: { ... }, // Optional additional context "retryAfter"?: number // Optional retry delay (seconds) } }
Examples:
// Validation error with field details { "error": { "code": "VALIDATION_ERROR", "message": "Validation failed", "details": { "fields": { "email": "Invalid email format", "phone": "Phone number is required" } } } } // Not found error { "error": { "code": "NOT_FOUND", "message": "Candidate not found" } } // Permission error { "error": { "code": "FORBIDDEN", "message": "You do not have permission to delete this job" } } // Rate limit error { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Too many API requests", "retryAfter": 60 } }
See examples/error-responses.ts.
3. Custom Error Classes
Create typed error classes for different scenarios:
// Base application error export class AppError extends Error { constructor( public code: string, message: string, public statusCode: number = 500, public details?: any, ) { super(message); this.name = "AppError"; } } // Validation errors (400) export class ValidationError extends AppError { constructor(message: string, details?: any) { super("VALIDATION_ERROR", message, 400, details); this.name = "ValidationError"; } } // Not found errors (404) export class NotFoundError extends AppError { constructor(resource: string) { super("NOT_FOUND", `${resource} not found`, 404); this.name = "NotFoundError"; } } // Permission errors (403) export class ForbiddenError extends AppError { constructor(message: string = "Access denied") { super("FORBIDDEN", message, 403); this.name = "ForbiddenError"; } } // Conflict errors (409) export class ConflictError extends AppError { constructor(message: string) { super("CONFLICT", message, 409); this.name = "ConflictError"; } } // State errors (422) export class InvalidStateError extends AppError { constructor(message: string) { super("INVALID_STATE", message, 422); this.name = "InvalidStateError"; } } // Usage throw new NotFoundError("Job"); throw new ValidationError("Invalid email", { field: "email" }); throw new ForbiddenError("Only recruiters can submit candidates"); throw new ConflictError("Application already exists"); throw new InvalidStateError("Cannot reopen closed job");
See examples/error-classes.ts.
4. Error Handler Middleware
Fastify error handler catches all errors:
// services/ats-service/src/index.ts app.setErrorHandler((error, request, reply) => { // Log error with context console.error("Error handling request:", { method: request.method, url: request.url, error: error.message, stack: error.stack, userId: request.headers["x-clerk-user-id"], }); // Handle custom AppError if (error instanceof AppError) { return reply.code(error.statusCode).send({ error: { code: error.code, message: error.message, details: error.details, }, }); } // Handle Fastify validation errors if (error.validation) { return reply.code(400).send({ error: { code: "VALIDATION_ERROR", message: "Request validation failed", details: error.validation, }, }); } // Handle Supabase errors if (error.code?.startsWith("PGRST")) { return reply.code(500).send({ error: { code: "DATABASE_ERROR", message: "Database operation failed", }, }); } // Fallback to 500 for unexpected errors return reply.code(500).send({ error: { code: "INTERNAL_SERVER_ERROR", message: "An unexpected error occurred", }, }); });
See examples/error-middleware.ts.
5. Frontend Error Handling
Handle API errors gracefully in frontend:
'use client'; import { useState } from 'react'; import { apiClient } from '@/lib/api-client'; export default function JobForm() { const [error, setError] = useState<string | null>(null); const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({}); const [submitting, setSubmitting] = useState(false); async function handleSubmit(data: any) { setError(null); setFieldErrors({}); setSubmitting(true); try { await apiClient.post('/jobs', data); // Success handling... } catch (err: any) { // Network error if (!err.response) { setError('Network error. Please check your connection.'); return; } const { error } = err.response.data; // Validation error with field details if (error.code === 'VALIDATION_ERROR' && error.details?.fields) { setFieldErrors(error.details.fields); setError('Please fix the validation errors below.'); } // Permission error else if (error.code === 'FORBIDDEN') { setError('You do not have permission to create jobs.'); } // Generic error else { setError(error.message || 'An unexpected error occurred.'); } } finally { setSubmitting(false); } } return ( <form onSubmit={handleSubmit}> {/* Global error alert */} {error && ( <div className="alert alert-error mb-4"> <i className="fa-duotone fa-regular fa-circle-exclamation"></i> <span>{error}</span> </div> )} {/* Field with error */} <fieldset className="fieldset"> <legend className="fieldset-legend">Job Title *</legend> <input type="text" className={`input w-full ${fieldErrors.title ? 'input-error' : ''}`} name="title" /> {fieldErrors.title && ( <p className="fieldset-label text-error">{fieldErrors.title}</p> )} </fieldset> <button type="submit" className="btn btn-primary" disabled={submitting}> {submitting ? 'Creating...' : 'Create Job'} </button> </form> ); }
See examples/frontend-error-handling.tsx.
6. Error Logging
Log errors with context for debugging:
// Backend error logging function logError( error: Error, context: { service: string; method: string; userId?: string; resourceId?: string; }, ): void { console.error("Error:", { service: context.service, method: context.method, userId: context.userId, resourceId: context.resourceId, error: { name: error.name, message: error.message, stack: error.stack, }, timestamp: new Date().toISOString(), }); } // Usage try { await repository.update(id, data); } catch (error) { logError(error as Error, { service: "ats-service", method: "JobRepository.update", userId: clerkUserId, resourceId: id, }); throw error; }
Logging Rules:
- ✅ Log all 500 errors with full context
- ✅ Include user ID and resource ID
- ✅ Include timestamp
- ✅ Include stack trace
- ⚠️ Log 400-level errors at info/warn level (not error)
- ❌ Don't log sensitive data (passwords, tokens)
See examples/error-logging.ts.
7. Async Error Handling
Handle errors in async operations:
// Try-catch for async functions async function fetchJob(id: string): Promise<Job> { try { const { data, error } = await supabase .from("jobs") .select("*") .eq("id", id) .single(); if (error) throw error; if (!data) throw new NotFoundError("Job"); return data; } catch (error) { // Log error console.error("Failed to fetch job:", error); throw error; // Re-throw for caller to handle } } // Promise.allSettled for parallel operations async function fetchMultipleJobs(ids: string[]): Promise<Job[]> { const results = await Promise.allSettled(ids.map((id) => fetchJob(id))); const jobs = results .filter( (r): r is PromiseFulfilledResult<Job> => r.status === "fulfilled", ) .map((r) => r.value); const errors = results .filter((r): r is PromiseRejectedResult => r.status === "rejected") .map((r) => r.reason); if (errors.length > 0) { console.warn(`Failed to fetch ${errors.length} jobs:`, errors); } return jobs; }
See examples/async-error-handling.ts.
8. Database Error Handling
Handle Supabase/PostgreSQL errors:
async function createJob(data: JobCreate): Promise<Job> { try { const { data: job, error } = await supabase .from("jobs") .insert(data) .select() .single(); if (error) { // Handle specific error codes switch (error.code) { case "23505": // Unique constraint violation throw new ConflictError( "Job with this title already exists", ); case "23503": // Foreign key violation throw new ValidationError("Invalid company ID"); case "23502": // Not null violation throw new ValidationError("Missing required field"); case "PGRST116": // Not found throw new NotFoundError("Job"); default: console.error("Database error:", error); throw new AppError( "DATABASE_ERROR", "Database operation failed", ); } } return job; } catch (error) { if (error instanceof AppError) throw error; console.error("Unexpected database error:", error); throw new AppError("DATABASE_ERROR", "Database operation failed"); } }
See examples/database-error-handling.ts and references/supabase-error-codes.md.
Error Code Catalog
Client Errors (4xx)
(400) - Request validation failedVALIDATION_ERROR
(401) - Authentication requiredUNAUTHORIZED
(403) - Insufficient permissionsFORBIDDEN
(404) - Resource not foundNOT_FOUND
(409) - Resource state conflictCONFLICT
(422) - Invalid resource stateINVALID_STATE
(429) - Rate limit exceededRATE_LIMIT_EXCEEDED
Server Errors (5xx)
(500) - Unexpected server errorINTERNAL_SERVER_ERROR
(500) - Database operation failedDATABASE_ERROR
(503) - External service unavailableSERVICE_UNAVAILABLE
See references/error-codes.md.
Testing Error Handling
Test error scenarios:
describe("JobRepository", () => { it("should throw NotFoundError for non-existent job", async () => { mockSupabase.single.mockResolvedValue({ data: null, error: null }); await expect(repository.getById("999")).rejects.toThrow(NotFoundError); }); it("should throw ConflictError for duplicate job", async () => { mockSupabase.insert.mockResolvedValue({ data: null, error: { code: "23505" }, }); await expect(repository.create(jobData)).rejects.toThrow(ConflictError); }); it("should return 404 for non-existent job", async () => { const response = await app.inject({ method: "GET", url: "/api/v2/jobs/non-existent-id", }); expect(response.statusCode).toBe(404); expect(JSON.parse(response.body)).toMatchObject({ error: { code: "NOT_FOUND", message: expect.any(String), }, }); }); });
See examples/error-testing.ts.
Anti-Patterns to Avoid
❌ Swallowing Errors
// WRONG - Silent failure try { await saveData(); } catch (error) { // Do nothing - error is lost! } // CORRECT - Log and handle try { await saveData(); } catch (error) { console.error("Failed to save data:", error); throw error; // Or handle appropriately }
❌ Generic Error Messages
// WRONG - Unhelpful throw new Error("Something went wrong"); // CORRECT - Specific throw new ValidationError("Email format is invalid");
❌ Exposing Stack Traces to Users
// WRONG - Security risk return reply.code(500).send({ error: error.stack, // Exposes internal details! }); // CORRECT - Generic message return reply.code(500).send({ error: { code: "INTERNAL_SERVER_ERROR", message: "An unexpected error occurred", }, });
❌ Not Using Status Codes
// WRONG - Always 200 return reply.send({ success: false, error: "Not found", }); // CORRECT - Use status code return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Job not found", }, });
References
- Error Classes
- Error Responses
- Error Middleware
- Frontend Error Handling
- Database Error Handling
- Async Error Handling
- Error Testing
- HTTP Status Codes
- Error Codes Catalog
- Supabase Error Codes
Related Skills
- API response format standardsapi-specifications
- Database error handlingdatabase-patterns
- Testing error scenariostesting-patterns