Error Handling
Overview
The framework provides structured error handling patterns that ensure consistent error management across all application layers. Error handling is designed to be explicit, type-safe, and informative.
Error Types
Validation Errors
Validation errors occur when input data doesn’t meet business rules or format requirements:
type ValidationError struct {
Field string
Message string
Value interface{}
}
Common scenarios: - Required fields missing - Invalid data formats - Business rule violations - Field length constraints
Database Errors
Database operations can fail for various reasons:
type DatabaseError struct {
Operation string
Table string
Err error
}
Common scenarios: - Connection failures - Constraint violations - Record not found - Transaction rollbacks
Business Logic Errors
Custom errors for specific business scenarios:
type BusinessError struct {
Code string
Message string
Context map[string]interface{}
}
Examples: - Insufficient permissions - Resource conflicts - State transition violations - Quota exceeded
Error Handling Layers
Service Layer
Services return structured errors that can be handled appropriately by calling layers:
Fetchers
- Return nil, false, nil
for not found (not an error)
- Return nil, false, error
for actual errors
- Distinguish between “not found” and “error occurred”
Handlers - Wrap database errors with operation context - Return specific errors for constraint violations - Handle transaction rollbacks gracefully
Validators - Return detailed validation errors with field information - Support multiple validation errors - Provide actionable error messages
Maestro Layer
Maestros orchestrate error handling across multiple services:
func (this *CreateGardenMae) Execute(ctx context.Context, input *CreateGardenMaeInput) (*CreateGardenMaeOutput, error) {
// Step 1: Validation
if err := this.GardenValidator.ValidateCreateForm(input.Form); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Step 2: Business logic
if err := this.checkBusinessRules(ctx, input.Form); err != nil {
return nil, fmt.Errorf("business rule violation: %w", err)
}
// Step 3: Persistence
garden, err := this.createGarden(ctx, input.Form)
if err != nil {
return nil, fmt.Errorf("failed to create garden: %w", err)
}
return &CreateGardenMaeOutput{Garden: garden}, nil
}
Controller Layer
Controllers translate errors into appropriate HTTP responses:
Error Response Mapping: - 400 Bad Request - Validation errors - 404 Not Found - Resource not found - 409 Conflict - Business rule violations - 500 Internal Server Error - System errors
Error Response Patterns
Structured Error Responses
Controllers return consistent error response formats:
func (gc *GardenController) handleError(c *gin.Context, err error) {
var validationErr *ValidationError
var businessErr *BusinessError
var dbErr *DatabaseError
switch {
case errors.As(err, &validationErr):
c.JSON(400, gin.H{
"error": "Validation failed",
"field": validationErr.Field,
"message": validationErr.Message,
"code": "VALIDATION_ERROR",
})
case errors.As(err, &businessErr):
c.JSON(409, gin.H{
"error": businessErr.Message,
"code": businessErr.Code,
"context": businessErr.Context,
})
case errors.As(err, &dbErr):
// Log detailed error, return generic message
log.Printf("Database error: %v", dbErr)
c.JSON(500, gin.H{
"error": "Internal server error",
"code": "INTERNAL_ERROR",
})
default:
// Unknown error - log and return generic response
log.Printf("Unknown error: %v", err)
c.JSON(500, gin.H{
"error": "Internal server error",
"code": "UNKNOWN_ERROR",
})
}
}
API Error Format
Standard API error response structure:
{
"error": "Human readable error message",
"code": "MACHINE_READABLE_CODE",
"field": "field_name",
"details": {
"additional": "context information"
}
}
HTML Error Handling
For web pages, errors are displayed in user-friendly formats:
func (gc *GardenController) Create(c *gin.Context) {
form := &forms.CreateGardenForm{}
if err := c.ShouldBind(form); err != nil {
c.HTML(400, "gardens/new.html", gin.H{
"error": "Please check your input and try again",
"form": form,
})
return
}
output, err := gc.CreateGardenMae.Execute(c.Request.Context(), &maes.CreateGardenMaeInput{
Form: form,
})
if err != nil {
c.HTML(400, "gardens/new.html", gin.H{
"error": err.Error(),
"form": form,
})
return
}
c.Redirect(302, "/gardens/" + output.Garden.Id)
}
Error Context and Wrapping
Error Wrapping
Use error wrapping to maintain error chains while adding context:
// In services
func (h *GardenHandler) Create(ctx context.Context, garden *mdl.Garden, mod *handler.HandlerMod) (*mdl.Garden, error) {
createdGarden, err := h.baseHandler.Create(ctx, garden, mod)
if err != nil {
return nil, fmt.Errorf("failed to create garden '%s': %w", garden.Name, err)
}
return createdGarden, nil
}
// In Maestros
func (m *CreateGardenMae) Execute(ctx context.Context, input *CreateGardenMaeInput) (*CreateGardenMaeOutput, error) {
garden, err := m.GardenHandler.Create(ctx, gardenEntity, nil)
if err != nil {
return nil, fmt.Errorf("garden creation failed for user %s: %w", input.UserId, err)
}
// ...
}
Context Information
Include relevant context in errors:
type ContextualError struct {
Operation string
EntityId string
UserId string
Timestamp time.Time
Err error
}
func (e *ContextualError) Error() string {
return fmt.Sprintf("operation '%s' failed for entity %s (user: %s) at %v: %v",
e.Operation, e.EntityId, e.UserId, e.Timestamp, e.Err)
}
Logging and Monitoring
Error Logging
Log errors with appropriate detail levels:
func (gc *GardenController) handleError(c *gin.Context, err error) {
requestID := c.GetString("request_id")
userID := c.GetString("user_id")
// Log with context
log.WithFields(log.Fields{
"request_id": requestID,
"user_id": userID,
"path": c.Request.URL.Path,
"error": err.Error(),
}).Error("Request failed")
// Return appropriate response...
}
Error Monitoring
Track error patterns for system health:
type ErrorTracker struct {
errorCounts map[string]int
mutex sync.RWMutex
}
func (et *ErrorTracker) RecordError(errorType string) {
et.mutex.Lock()
defer et.mutex.Unlock()
et.errorCounts[errorType]++
}
func (et *ErrorTracker) GetErrorStats() map[string]int {
et.mutex.RLock()
defer et.mutex.RUnlock()
stats := make(map[string]int)
for k, v := range et.errorCounts {
stats[k] = v
}
return stats
}
Recovery and Resilience
Panic Recovery
Handle panics gracefully in middleware:
func RecoveryMiddleware() gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
if err, ok := recovered.(string); ok {
log.Printf("Panic recovered: %s", err)
}
c.JSON(500, gin.H{
"error": "Internal server error",
"code": "PANIC_RECOVERED",
})
})
}
Transaction Rollback
Ensure database consistency during errors:
func (m *ComplexGardenMae) Execute(ctx context.Context, input *ComplexGardenMaeInput) (*ComplexGardenMaeOutput, error) {
tx, err := m.SqlDb.Begin(ctx)
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback() // Rollback if not committed
handlerMod := &handler.HandlerMod{Tx: tx}
// Perform operations...
garden, err := m.GardenHandler.Create(ctx, gardenEntity, handlerMod)
if err != nil {
return nil, fmt.Errorf("failed to create garden: %w", err)
}
// Commit transaction
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
return &ComplexGardenMaeOutput{Garden: garden}, nil
}
Testing Error Scenarios
Unit Testing Errors
Test error conditions explicitly:
func TestCreateGardenMae_ValidationError(t *testing.T) {
mae := setupCreateGardenMae()
input := &maes.CreateGardenMaeInput{
Form: &forms.CreateGardenForm{
Name: "", // Invalid - empty name
},
}
output, err := mae.Execute(context.Background(), input)
assert.Error(t, err)
assert.Nil(t, output)
var validationErr *ValidationError
assert.True(t, errors.As(err, &validationErr))
assert.Equal(t, "name", validationErr.Field)
}
Integration Testing
Test complete error flows:
func TestGardenController_CreateValidationError(t *testing.T) {
container := setupTestContainer()
router := setupTestRouter(container)
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/gardens", strings.NewReader(`{"name": ""}`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, 400, w.Code)
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
assert.Equal(t, "VALIDATION_ERROR", response["code"])
}
Best Practices
Error Design Principles
- Be Explicit - Don’t hide errors or fail silently
- Be Specific - Provide actionable error information
- Be Consistent - Use standard error types and responses
- Be Secure - Don’t expose sensitive information in errors
- Be Helpful - Include context and suggestions when possible
Common Patterns
- Fail Fast - Validate early and return errors immediately
- Error Boundaries - Handle errors at appropriate layers
- Graceful Degradation - Provide fallbacks when possible
- User-Friendly Messages - Translate technical errors for end users
- Comprehensive Logging - Log enough detail for debugging
Error handling is fundamental to building reliable applications. The framework’s structured approach ensures consistent, maintainable error management across all application layers.