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:
- Fail Fast: Detect and report errors as early as possible
- Explicit Error Types: Use domain-specific error types rather than generic exceptions
- Graceful Degradation: Continue operating with reduced functionality when possible
- User-Friendly Messages: Provide clear, actionable error messages to users
- 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.