4.1 KiB
Error Rules (ERR-01..05)
Three-Tier Error Architecture
The error system has three tiers:
- Domain errors — sentinel variables and typed structs in
domain/ - Application errors —
SlugErrorwith machine-readable slugs inapp/ - Port errors — protocol-specific error mapping in
ports/
ERR-01: Sentinel Error Variables (WARNING)
Simple domain errors without context SHOULD use sentinel var declarations.
// domain/hour/errors.go
var (
ErrNotFullHour = errors.New("hour should be a full hour")
ErrPastHour = errors.New("cannot create hour in the past")
ErrTrainingScheduled = errors.New("unable to modify hour, because scheduled training")
ErrHourNotAvailable = errors.New("hour is not available")
ErrNoTrainingScheduled = errors.New("no training scheduled")
)
Naming: Err{DescriptiveName} — always starts with Err.
Usage in domain methods:
func (h *Hour) ScheduleTraining() error {
if !h.IsAvailable() {
return ErrHourNotAvailable
}
h.availability = TrainingScheduled
return nil
}
ERR-02: Typed Error Structs (WARNING)
Errors that carry context (values for logging/display) SHOULD be typed structs implementing the error interface.
type TooDistantDateError struct {
MaxWeeksInTheFutureToSet int
ProvidedDate time.Time
}
func (e TooDistantDateError) Error() string {
return fmt.Sprintf(
"schedule can be only set for next %d weeks, provided date: %s",
e.MaxWeeksInTheFutureToSet, e.ProvidedDate,
)
}
type TooEarlyHourError struct {
MinUtcHour int
ProvidedTime time.Time
}
type ForbiddenToSeeTrainingError struct {
RequestingUserUUID string
TrainingOwnerUUID string
}
type NotFoundError struct {
TrainingUUID string
}
Naming: {Condition}Error — describes the error condition.
ERR-03: SlugError for Application Layer (WARNING)
Application-layer errors (command/query handlers) SHOULD use SlugError from the common errors package. SlugErrors carry:
- Human-readable error message
- Machine-readable slug (used by API clients)
- Error type (authorization, incorrect-input, unknown)
// common/errors/errors.go
type ErrorType struct {
t string
}
var (
ErrorTypeUnknown = ErrorType{"unknown"}
ErrorTypeAuthorization = ErrorType{"authorization"}
ErrorTypeIncorrectInput = ErrorType{"incorrect-input"}
)
type SlugError struct {
error string
slug string
errorType ErrorType
}
func NewSlugError(error string, slug string) SlugError
func NewAuthorizationError(error string, slug string) SlugError
func NewIncorrectInputError(error string, slug string) SlugError
Usage in handlers:
func (h cancelTrainingHandler) Handle(ctx context.Context, cmd CancelTraining) error {
if err := h.hourRepo.UpdateHour(ctx, cmd.Hour, func(h *hour.Hour) (*hour.Hour, error) {
if err := h.CancelTraining(); err != nil {
return nil, err
}
return h, nil
}); err != nil {
return errors.NewSlugError(err.Error(), "unable-to-update-availability")
}
return nil
}
ERR-04: Error Wrapping with Context (INFO)
When re-raising errors, wrap them with context using fmt.Errorf("context: %w", err) or a wrapping library.
// In adapters
if err := doc.DataTo(&model); err != nil {
return nil, fmt.Errorf("unmarshaling hour from firestore: %w", err)
}
ERR-05: No Bare fmt.Errorf in Domain (CRITICAL)
The domain package MUST NOT use fmt.Errorf for error creation. Domain errors must be either:
- Sentinel variables (
var ErrX = errors.New(...)) - Typed error structs
- Standard
errors.New(...)for simple cases
Check: Grep domain/ for fmt.Errorf. Any match in non-test files is a violation.
Rationale: fmt.Errorf creates untyped errors that cannot be checked with errors.Is or errors.As. Domain errors should be programmatically handleable.
Exception: fmt.Errorf with %w for wrapping IS acceptable in domain validation helpers that combine multiple checks, but prefer typed errors or sentinel variables.