| name | brokle-error-handling |
| description | Use this skill when implementing, reviewing, or debugging error handling in the Brokle codebase. This includes repository error mapping, service layer AppError constructors, handler response mapping, domain alias imports, or understanding the industrial error handling flow. |
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 patternsdocs/development/DOMAIN_ALIAS_PATTERNS.md- Professional import patternsdocs/development/ERROR_HANDLING_QUICK_REFERENCE.md- Developer cheat sheet