-
Notifications
You must be signed in to change notification settings - Fork 0
Error Handling in TicketHive
This document describes the error handling architecture and patterns used throughout the TicketHive application.
TicketHive implements a structured, type-safe error handling system that:
- Provides user-friendly error messages
- Maintains consistent error responses across the API
- Distinguishes between business logic errors and infrastructure errors
- Handles database-level errors gracefully
- Supports proper HTTP status codes and error categorization
The application uses a three-layer approach to error handling:
-
Database Layer (PostgreSQL)
- Throws low-level technical errors with PostgreSQL error codes
- Examples:
57014(statement timeout),23505(unique constraint violation)
-
Service Layer (
src/services/)- Catches database errors and converts them to business-friendly
AppErrorinstances - Throws
AppErrorfor business logic violations - Examples:
EVENT_NOT_FOUND,EVENT_SOLD_OUT
- Catches database errors and converts them to business-friendly
-
Route Layer (
src/routes/)- Catches all errors and formats them as HTTP responses
- Uses
handleErrorfunction for consistent error formatting - Maps errors to appropriate HTTP status codes
These are expected errors caused by invalid user requests or business rule violations. They indicate the user did something wrong or tried to do something not allowed.
| Error Code | Status | Meaning | User Action |
|---|---|---|---|
EVENT_NOT_FOUND |
404 | Event doesn't exist | Check event ID |
EVENT_SOLD_OUT |
409 | No tickets available | Try different event |
BOOKING_ALREADY_CANCELLED |
409 | Already cancelled | Don't retry |
INVALID_CREDENTIALS |
401 | Wrong email/password | Check credentials |
EMAIL_ALREADY_REGISTERED |
409 | Email taken | Use different email |
INVALID_TOKEN |
401 | Invalid or expired token | Re-authenticate |
UNAUTHORIZED |
403 | Unauthorized access | Check permissions |
Characteristics:
- Part of normal operation
- Should NOT trigger alerts
- User can fix by changing their request
- Should NOT be retried (same request will fail again)
- Logged for analytics but not as errors
These indicate system problems - not user mistakes. Something is wrong with the infrastructure (database, network, etc.) or the application itself.
| Error Code | Status | Meaning | User Action |
|---|---|---|---|
STATEMENT_TIMEOUT |
503 | Database overloaded | Wait and retry |
DATABASE_CONNECTION_ERROR |
503 | Can't reach database | Wait and retry |
INTERNAL_SERVER_ERROR |
500 | Unexpected bug | Report issue |
FAILED_TO_CREATE_EVENT |
500 | Event creation failed | Report issue |
FAILED_TO_CREATE_USER |
500 | User creation failed | Report issue |
Characteristics:
- Exceptional and should be rare
- Should trigger monitoring alerts
- Not the user's fault
- CAN be retried (may succeed on retry)
- Logged as errors for investigation
- May indicate need for scaling/optimization
All application errors are defined in src/lib/errors.ts using a centralized ErrorCode object:
export const ErrorCode = {
// Authentication errors
INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
EMAIL_ALREADY_REGISTERED: "EMAIL_ALREADY_REGISTERED",
INVALID_TOKEN: "INVALID_TOKEN",
UNAUTHORIZED: "UNAUTHORIZED",
// Booking errors
EVENT_NOT_FOUND: "EVENT_NOT_FOUND",
EVENT_SOLD_OUT: "EVENT_SOLD_OUT",
BOOKING_NOT_FOUND: "BOOKING_NOT_FOUND",
BOOKING_ALREADY_CANCELLED: "BOOKING_ALREADY_CANCELLED",
// Event errors
FAILED_TO_CREATE_EVENT: "FAILED_TO_CREATE_EVENT",
FAILED_TO_CREATE_USER: "FAILED_TO_CREATE_USER",
// Database timeout errors
STATEMENT_TIMEOUT: "STATEMENT_TIMEOUT",
DATABASE_CONNECTION_ERROR: "DATABASE_CONNECTION_ERROR",
// Unknown/generic errors
INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR",
} as const;The custom AppError class extends the standard Error with additional properties:
export class AppError extends Error {
public code: ErrorCode;
public statusCode: number;
constructor(
code: ErrorCode,
statusCode: number = ERROR_METADATA[code].statusCode,
message?: string,
) {
super(message || ERROR_METADATA[code].message);
this.code = code;
this.statusCode = statusCode;
this.name = "AppError";
}
static isAppError(error: unknown): error is AppError {
return error instanceof AppError;
}
getStatusCode(): number {
return this.statusCode;
}
}Each error code maps to HTTP status codes and user-friendly messages:
export const ERROR_METADATA: Record<
ErrorCode,
{ statusCode: number; message: string }
> = {
[ErrorCode.INVALID_CREDENTIALS]: {
statusCode: 401,
message: "Invalid email or password",
},
[ErrorCode.EVENT_NOT_FOUND]: {
statusCode: 404,
message: "Event not found",
},
// ... more mappings
};The system includes helper functions to detect specific PostgreSQL errors:
-
isPostgresTimeoutError(error)- Detects statement timeouts (code 57014) -
isPostgresUniqueConstraintError(error)- Detects unique constraint violations (code 23505) -
isPostgresForeignKeyViolationError(error)- Detects foreign key violations (code 23503) -
isConnectionTimeoutError(error)- Detects connection timeouts (code ETIMEDOUT)
The database connection in src/lib/db.ts is configured with:
- Connection Pool: 20 max connections, 30s idle timeout
- Connect Timeout: 10 seconds for initial connection
- Statement Timeout: 5 seconds for query execution
This configuration prevents indefinite lock waits and provides fast failure under high contention.
All error responses follow a consistent JSON format:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "User-friendly error message"
}
}Business Logic Error:
{
"success": false,
"error": {
"code": "EVENT_SOLD_OUT",
"message": "Event is sold out"
}
}Infrastructure Error:
{
"success": false,
"error": {
"code": "STATEMENT_TIMEOUT",
"message": "High traffic detected. Please try again in a moment."
}
}Validation Error:
{
"success": false,
"error": {
"code": "INVALID_CREDENTIALS",
"message": "Validation failed"
},
"details": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": ["email"],
"message": "Required"
}
]
}Services should throw AppError instances for business logic violations:
// Check if event exists
if (events.length === 0) {
throw new AppError(ErrorCode.EVENT_NOT_FOUND);
}
// Check ticket availability
if (event.available_tickets <= 0) {
throw new AppError(ErrorCode.EVENT_SOLD_OUT);
}Services should catch and convert database errors to appropriate AppError instances:
try {
// Database operation that might fail
const result = await sql`...`;
} catch (error) {
if (isPostgresUniqueConstraintError(error)) {
throw new AppError(ErrorCode.EMAIL_ALREADY_REGISTERED);
}
if (isPostgresForeignKeyViolationError(error)) {
throw new AppError(ErrorCode.EVENT_NOT_FOUND);
}
// Let other database errors propagate to route handler
throw error;
}Routes should use the centralized handleError function:
import { handleError } from "../lib/errorHandler.ts";
export async function createBooking(req: Request, res: Response) {
try {
// Business logic
const booking = await bookingService.createBooking(bookingData);
res.status(201).json({
success: true,
data: booking,
message: "Booking created successfully"
});
} catch (error) {
handleError(error, res, "createBooking");
}
}Never expose technical error details to end users. Convert technical errors to business-friendly messages.
❌ Technical:
PostgresError: canceling statement due to statement timeout
✅ User-Friendly:
{
"code": "STATEMENT_TIMEOUT",
"message": "High traffic detected. Please try again in a moment."
}Use the centralized ErrorCode object instead of string literals to ensure consistency.
- 4xx: User/client errors (business logic violations)
- 5xx: Server/infrastructure errors (system problems)
- 503: Temporary unavailability (retry recommended)
- Log business logic errors at INFO level for analytics
- Log infrastructure errors at ERROR level for investigation
- Include context in error logs for debugging
- Do retry: Infrastructure errors (503 status)
- Don't retry: Business logic errors (4xx status)
- Error Rate: Percentage of requests that result in errors
- Error Distribution: Breakdown by error code and category
- Timeout Rate: Percentage of database timeouts
- Response Time: P95 and P99 response times
- High rate of infrastructure errors (>5%)
- Database connection pool exhaustion
- Statement timeout rate >20% (consider Level 3 implementation)
- Any 500 Internal Server Errors
When the current error handling becomes a bottleneck (>20% timeout rate), consider implementing:
- Job Queues: Use BullMQ for asynchronous booking processing
- 202 Accepted: Return immediate acceptance, process in background
- Event Sourcing: Track booking attempts as events
- Circuit Breaker: Prevent cascading failures during database outages
-
src/lib/errors.ts- Core error definitions and utilities -
src/lib/errorHandler.ts- Centralized error handling function -
src/lib/db.ts- Database configuration with timeouts -
src/services/*.ts- Service-layer error handling -
src/routes/*.ts- Route-layer error handling