Skip to content

Error Handling Patterns

This document outlines the error handling strategies, patterns, and domain-specific error types used throughout the Bartendie system.

The Bartendie system follows these principles for error handling:

  1. Fail Fast: Detect and report errors as early as possible
  2. Explicit Error Types: Use domain-specific error types rather than generic exceptions
  3. Graceful Degradation: Continue operating with reduced functionality when possible
  4. User-Friendly Messages: Provide clear, actionable error messages to users
  5. Comprehensive Logging: Log all errors with sufficient context for debugging
classDiagram
    class EventError {
        <<Abstract>>
        +ErrorCode String
        +Message String
        +Timestamp DateTime
        +GetUserMessage() String
    }
    
    class EventDateError {
        +EventDate DateTime
        +MinimumDate DateTime
    }
    
    class VenueCapacityError {
        +RequestedCapacity Integer
        +MaxCapacity Integer
        +VenueId UUID
    }
    
    class EventConflictError {
        +ConflictingEventId UUID
        +ConflictingDate DateTime
        +VenueId UUID
    }
    
    class GuestListError {
        +DuplicateGuestId UUID
        +InvalidRSVPTransition String
    }
    
    EventError <|-- EventDateError
    EventError <|-- VenueCapacityError
    EventError <|-- EventConflictError
    EventError <|-- GuestListError
classDiagram
    class RecipeError {
        <<Abstract>>
        +RecipeId UUID
        +ErrorCode String
        +Message String
    }
    
    class IngredientNotFoundError {
        +IngredientName String
        +SuggestedAlternatives List~String~
    }
    
    class RecipeBalanceError {
        +CurrentBalance RecipeBalance
        +RecommendedAdjustments List~String~
    }
    
    class MenuValidationError {
        +InvalidItems List~DrinkItem~
        +MissingIngredients List~String~
    }
    
    class AlcoholContentError {
        +CurrentPercentage Float
        +MaxAllowedPercentage Float
    }
    
    RecipeError <|-- IngredientNotFoundError
    RecipeError <|-- RecipeBalanceError
    RecipeError <|-- MenuValidationError
    RecipeError <|-- AlcoholContentError
classDiagram
    class InventoryError {
        <<Abstract>>
        +ProductId UUID
        +ErrorCode String
        +Message String
    }
    
    class InsufficientStockError {
        +RequiredQuantity Decimal
        +AvailableQuantity Decimal
        +Unit MeasurementUnit
    }
    
    class ProductExpiredError {
        +ExpirationDate DateTime
        +DaysExpired Integer
    }
    
    class BarcodeError {
        +Barcode String
        +ErrorType BarcodeErrorType
    }
    
    class SupplierError {
        +SupplierId UUID
        +SupplierName String
        +ErrorType SupplierErrorType
    }
    
    InventoryError <|-- InsufficientStockError
    InventoryError <|-- ProductExpiredError
    InventoryError <|-- BarcodeError
    InventoryError <|-- SupplierError
classDiagram
    class SecurityError {
        <<Abstract>>
        +UserId UUID
        +Resource String
        +Action String
        +ErrorCode String
    }
    
    class AuthenticationError {
        +Username String
        +FailureReason String
        +AttemptCount Integer
    }
    
    class AuthorizationError {
        +RequiredPermission String
        +UserRoles List~String~
    }
    
    class ValidationError {
        +FieldName String
        +FieldValue String
        +ValidationRule String
    }
    
    SecurityError <|-- AuthenticationError
    SecurityError <|-- AuthorizationError
    SecurityError <|-- ValidationError

Use the Result pattern for operations that can fail:

// Example Result pattern implementation
class Result<T, E> {
constructor(
private readonly value?: T,
private readonly error?: E,
private readonly isSuccess: boolean = true
) {}
static success<T>(value: T): Result<T, never> {
return new Result(value, undefined, true);
}
static failure<E>(error: E): Result<never, E> {
return new Result(undefined, error, false);
}
isOk(): boolean {
return this.isSuccess;
}
isErr(): boolean {
return !this.isSuccess;
}
getValue(): T {
if (!this.isSuccess) {
throw new Error("Cannot get value from failed result");
}
return this.value!;
}
getError(): E {
if (this.isSuccess) {
throw new Error("Cannot get error from successful result");
}
return this.error!;
}
}

Validate commands before processing:

class CreateEventCommand {
validate(): Result<void, ValidationError[]> {
const errors: ValidationError[] = [];
if (this.date <= new Date()) {
errors.push(new ValidationError("date", this.date.toString(), "must_be_future"));
}
if (this.guestCount <= 0) {
errors.push(new ValidationError("guestCount", this.guestCount.toString(), "must_be_positive"));
}
return errors.length > 0
? Result.failure(errors)
: Result.success(undefined);
}
}

Protect aggregate invariants with domain exceptions:

class Event {
addGuest(guest: Guest): Result<void, EventError> {
if (this.guests.length >= this.venue.maxCapacity) {
return Result.failure(new VenueCapacityError(
this.guests.length + 1,
this.venue.maxCapacity,
this.venue.id
));
}
if (this.guests.some(g => g.id === guest.id)) {
return Result.failure(new GuestListError(guest.id, "duplicate_guest"));
}
this.guests.push(guest);
return Result.success(undefined);
}
}

Handle external system failures gracefully:

class ProductLookupService {
async lookupProduct(barcode: string): Promise<Result<ProductData, InventoryError>> {
try {
const response = await this.externalProductAPI.lookup(barcode);
return Result.success(this.mapToProductData(response));
} catch (error) {
if (error instanceof NetworkError) {
// Fallback to cached data or manual entry
return this.handleNetworkFailure(barcode);
}
if (error instanceof NotFoundError) {
return Result.failure(new BarcodeError(barcode, "product_not_found"));
}
// Log unexpected errors
this.logger.error("Unexpected error in product lookup", { barcode, error });
return Result.failure(new BarcodeError(barcode, "lookup_failed"));
}
}
private async handleNetworkFailure(barcode: string): Promise<Result<ProductData, InventoryError>> {
const cachedData = await this.cache.get(barcode);
if (cachedData) {
this.notificationService.warn("Using cached product data due to network issues");
return Result.success(cachedData);
}
return Result.failure(new BarcodeError(barcode, "network_unavailable"));
}
}

For failed operations that require cleanup:

class MenuCreationService {
async createMenu(command: CreateMenuCommand): Promise<Result<Menu, MenuError>> {
const compensationActions: (() => Promise<void>)[] = [];
try {
// Step 1: Create menu
const menu = await this.menuRepository.create(command.toMenu());
compensationActions.push(() => this.menuRepository.delete(menu.id));
// Step 2: Reserve ingredients
const reservationResult = await this.inventoryService.reserveIngredients(command.ingredients);
if (reservationResult.isErr()) {
await this.executeCompensation(compensationActions);
return Result.failure(reservationResult.getError());
}
compensationActions.push(() => this.inventoryService.releaseReservation(reservationResult.getValue()));
// Step 3: Validate menu balance
const balanceResult = this.validateMenuBalance(menu);
if (balanceResult.isErr()) {
await this.executeCompensation(compensationActions);
return Result.failure(balanceResult.getError());
}
return Result.success(menu);
} catch (error) {
await this.executeCompensation(compensationActions);
throw error;
}
}
private async executeCompensation(actions: (() => Promise<void>)[]): Promise<void> {
for (const action of actions.reverse()) {
try {
await action();
} catch (compensationError) {
this.logger.error("Compensation action failed", { compensationError });
}
}
}
}

Prevent cascading failures from external systems:

class CircuitBreaker {
private failureCount = 0;
private lastFailureTime?: Date;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private readonly failureThreshold: number = 5,
private readonly timeoutMs: number = 60000
) {}
async execute<T>(operation: () => Promise<T>): Promise<Result<T, Error>> {
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
} else {
return Result.failure(new Error("Circuit breaker is OPEN"));
}
}
try {
const result = await operation();
this.onSuccess();
return Result.success(result);
} catch (error) {
this.onFailure();
return Result.failure(error);
}
}
private onSuccess(): void {
this.failureCount = 0;
this.state = 'CLOSED';
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = new Date();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
}
private shouldAttemptReset(): boolean {
return this.lastFailureTime &&
(Date.now() - this.lastFailureTime.getTime()) > this.timeoutMs;
}
}
  • CRITICAL: System-wide failures, data corruption, security breaches
  • HIGH: Feature unavailability, external system failures
  • MEDIUM: Validation errors, business rule violations
  • LOW: User input errors, expected failures
class ErrorMonitor {
logError(error: Error, context: ErrorContext): void {
const errorData = {
id: generateId(),
timestamp: new Date(),
severity: this.determineSeverity(error),
type: error.constructor.name,
message: error.message,
stackTrace: error.stack,
context: context,
userId: context.userId,
correlationId: context.correlationId
};
// Log to structured logging system
this.logger.error(errorData);
// Send to monitoring system
this.metricsCollector.incrementCounter('errors_total', {
type: error.constructor.name,
severity: errorData.severity
});
// Trigger alerts for critical errors
if (errorData.severity === 'CRITICAL') {
this.alertingService.sendAlert(errorData);
}
}
private determineSeverity(error: Error): string {
if (error instanceof SecurityError) return 'CRITICAL';
if (error instanceof InventoryError) return 'HIGH';
if (error instanceof ValidationError) return 'LOW';
return 'MEDIUM';
}
}

This comprehensive error handling approach ensures the Bartendie system can gracefully handle failures while providing clear feedback to users and maintaining system stability.