Skip to main content

Error Handling Patterns

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

Error Handling Philosophy

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

Domain Error Types

Event Planning Errors

Recipe and Menu Errors

Inventory and Shopping Errors

User and Security Errors

Error Handling Patterns

1. Result Pattern

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!;
}
}

2. Command Validation Pattern

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);
}
}

3. Aggregate Invariant Protection

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);
}
}

4. External System Error Handling

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"));
}
}

Error Recovery Strategies

1. Compensation Actions

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 });
}
}
}
}

2. Circuit Breaker Pattern

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;
}
}

Error Monitoring and Alerting

Error Severity Levels

  • 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

Monitoring Patterns

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.