Brokle Error Handling Skill
Expert guidance for implementing industrial error handling patterns across all layers.
Industrial Error Handling Pattern
Core Flow:
Repository (Domain Errors) → Service (AppErrors) → Handler (HTTP Response)
Three-Layer Clean Architecture:
- •Repository Layer: Domain errors with context wrapping
- •Service Layer: AppError constructors for business logic
- •Handler Layer: Centralized
response.Error()handling - •Zero Logging: Core services focus on pure business logic
Layer Responsibilities
| Layer | Responsibility | Error Pattern | Example |
|---|---|---|---|
| Repository | Data access, domain error wrapping | fmt.Errorf("context: %w", domainError) | fmt.Errorf("get user by ID %s: %w", id, userDomain.ErrNotFound) |
| Service | Business logic, AppError constructors | appErrors.NewNotFoundError() | return nil, appErrors.NewNotFoundError("User not found") |
| Handler | HTTP transport, response mapping | response.Error(c, err) | Automatic HTTP status code mapping |
Domain Alias Pattern (MANDATORY)
// ✅ Correct - Professional domain alias pattern
import (
"context"
"fmt"
"gorm.io/gorm"
authDomain "brokle/internal/core/domain/auth"
orgDomain "brokle/internal/core/domain/organization"
userDomain "brokle/internal/core/domain/user"
"brokle/pkg/ulid"
)
// ❌ Incorrect - Direct domain imports
import (
"brokle/internal/core/domain/auth"
)
Standard Aliases:
| Domain | Alias | Usage |
|---|---|---|
| auth | authDomain | authDomain.User, authDomain.ErrNotFound |
| organization | orgDomain | orgDomain.Organization |
| user | userDomain | userDomain.User |
| billing | billingDomain | billingDomain.Subscription |
| observability | obsDomain | obsDomain.Trace |
Repository Layer Pattern
package auth
import (
"context"
"fmt"
"gorm.io/gorm"
authDomain "brokle/internal/core/domain/auth"
"brokle/pkg/ulid"
)
type userRepository struct {
db *gorm.DB
}
// ✅ Correct pattern
func (r *userRepository) GetByID(ctx context.Context, id ulid.ULID) (*authDomain.User, error) {
var user authDomain.User
err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { // ✅ Standardized pattern
return nil, fmt.Errorf("get user by ID %s: %w", id, authDomain.ErrNotFound)
}
return nil, fmt.Errorf("database query failed for user ID %s: %w", id, err)
}
return &user, nil
}
// ✅ Correct pattern
func (r *userRepository) Create(ctx context.Context, user *authDomain.User) error {
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
return fmt.Errorf("create user %s: %w", user.Email, err)
}
return nil
}
Repository Requirements (internal/infrastructure/repository/user/user_repository.go:37-54):
✅ Required Pattern:
// 1. Professional domain alias imports
import (
"errors"
"fmt"
"gorm.io/gorm"
userDomain "brokle/internal/core/domain/user"
)
// 2. GORM error checking with errors.Is() (STANDARD PATTERN)
func (r *userRepository) GetByID(ctx context.Context, id ulid.ULID) (*userDomain.User, error) {
var u userDomain.User
err := r.db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id).First(&u).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { // ✅ Standardized across all repos
return nil, fmt.Errorf("get user by ID %s: %w", id, userDomain.ErrNotFound)
}
return nil, fmt.Errorf("database query failed for user ID %s: %w", id, err)
}
return &u, nil
}
Standard: All repositories use errors.Is() for GORM error checking (standardized across codebase for wrapped error compatibility).
Service Layer Pattern
package auth
import (
"context"
"errors"
authDomain "brokle/internal/core/domain/auth"
"brokle/pkg/apperrors"
"brokle/pkg/ulid"
)
type userService struct {
userRepo authDomain.UserRepository
}
// ✅ Correct pattern
func (s *userService) GetUser(ctx context.Context, id ulid.ULID) (*GetUserResponse, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
// Check for domain errors and convert to AppErrors
if errors.Is(err, authDomain.ErrNotFound) {
return nil, appErrors.NewNotFoundError("User") // resource string
}
return nil, appErrors.NewInternalError("Failed to retrieve user", err)
}
return &GetUserResponse{User: user}, nil
}
// ✅ Correct pattern with validation
func (s *userService) CreateUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
// Input validation - NewValidationError(message, details string)
if err := req.Validate(); err != nil {
return nil, appErrors.NewValidationError("Invalid user data", err.Error())
}
// Check for existing user
existingUser, err := s.userRepo.GetByEmail(ctx, req.Email)
if err != nil && !errors.Is(err, authDomain.ErrNotFound) {
return nil, appErrors.NewInternalError("Failed to check existing user", err)
}
if existingUser != nil {
return nil, appErrors.NewConflictError("User already exists with this email")
}
// Create user
user := &authDomain.User{
ID: ulid.New(),
Email: req.Email,
Name: req.Name,
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, appErrors.NewInternalError("Failed to create user", err)
}
return &CreateUserResponse{User: user}, nil
}
Service Requirements:
✅ Required (with exact signatures):
// 1. AppError constructors for business logic
if errors.Is(err, userDomain.ErrNotFound) {
return nil, appErrors.NewNotFoundError("User") // resource string
}
// 2. Validation errors (message, details string)
return nil, appErrors.NewValidationError("Invalid email format", "email must be valid")
// 3. Internal errors (message string, err error)
return nil, appErrors.NewInternalError("Failed to process request", err)
❌ Prohibited:
// Don't use fmt.Errorf or errors.New in services
return nil, fmt.Errorf("user not found") // ❌
// Don't include logging in core services
log.Error("Failed to create user") // ❌
AppError Constructors (see pkg/errors/errors.go:86-136 for complete list):
Common constructors:
appErrors.NewValidationError(message, details string) *AppError appErrors.NewNotFoundError(resource string) *AppError appErrors.NewConflictError(message string) *AppError appErrors.NewUnauthorizedError(message string) *AppError appErrors.NewForbiddenError(message string) *AppError appErrors.NewInternalError(message string, err error) *AppError appErrors.NewRateLimitError(message string) *AppError // Helper appErrors.IsAppError(err error) (*AppError, bool)
Reference: See pkg/errors/errors.go for all constructors (BadRequest, ServiceUnavailable, NotImplemented, PaymentRequired, QuotaExceeded, AIProvider) and exact signatures
Handler Layer Pattern
package http
import (
"github.com/gin-gonic/gin"
authDomain "brokle/internal/core/domain/auth"
"brokle/pkg/response"
"brokle/pkg/ulid"
"brokle/pkg/apperrors"
)
type userHandler struct {
userService authDomain.UserService
}
// ✅ Correct pattern
func (h *userHandler) GetUser(c *gin.Context) {
// 1. Validate input
userID := c.Param("id")
id, err := ulid.Parse(userID)
if err != nil {
response.Error(c, appErrors.NewValidationError("Invalid user ID format", "id must be a valid ULID"))
return
}
// 2. Call service
resp, err := h.userService.GetUser(c.Request.Context(), id)
if err != nil {
response.Error(c, err) // Automatic HTTP status mapping
return
}
// 3. Return success
response.Success(c, resp)
}
Handler Requirements:
✅ Required:
// 1. Structured response handling
resp, err := h.service.Method(c, req)
if err != nil {
response.Error(c, err) // Automatic status mapping
return
}
response.Success(c, resp)
// 2. Input validation before service calls
if err := req.Validate(); err != nil {
response.Error(c, appErrors.NewValidationError("Invalid request", err))
return
}
❌ Prohibited:
// Don't inspect or log errors manually
if errors.Is(err, apperrors.ErrNotFound) { // ❌
c.JSON(404, gin.H{"error": "Not found"})
}
// Always use response.Error()
response.Error(c, err) // ✅
HTTP Status Code Mapping
| AppError Type | HTTP Status | Description |
|---|---|---|
| ValidationError | 400 Bad Request | Invalid input data |
| UnauthorizedError | 401 Unauthorized | Authentication required |
| ForbiddenError | 403 Forbidden | Insufficient permissions |
| NotFoundError | 404 Not Found | Resource not found |
| ConflictError | 409 Conflict | Resource already exists |
| RateLimitError | 429 Too Many Requests | Rate limit exceeded |
| InternalError | 500 Internal Server Error | Unexpected server error |
Standard Error Response Format
{
"error": {
"type": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"email": "must be a valid email address",
"password": "must be at least 8 characters"
},
"code": "INVALID_INPUT",
"request_id": "req_01H4XJZQX3EXAMPLE"
}
}
Best Practices Checklist
Repository Layer:
- • Use professional domain alias imports
- • Convert GORM errors to domain errors with context
- • No
errors.New()or business logic - • All database operations include error context
Service Layer:
- • Use AppError constructors only
- • Convert domain errors to business errors
- • No logging in core services
- • Validate inputs with meaningful error messages
Handler Layer:
- • Use
response.Error()for all error responses - • Validate input before calling services
- • No business logic in handlers
- • Return appropriate HTTP status codes
References
- •
docs/development/ERROR_HANDLING_GUIDE.md- Complete industrial patterns - •
docs/development/DOMAIN_ALIAS_PATTERNS.md- Professional import patterns - •
docs/development/ERROR_HANDLING_QUICK_REFERENCE.md- Developer cheat sheet