151 lines
4.1 KiB
Markdown
151 lines
4.1 KiB
Markdown
# Error Rules (ERR-01..05)
|
|
|
|
## Three-Tier Error Architecture
|
|
|
|
The error system has three tiers:
|
|
|
|
1. **Domain errors** — sentinel variables and typed structs in `domain/`
|
|
2. **Application errors** — `SlugError` with machine-readable slugs in `app/`
|
|
3. **Port errors** — protocol-specific error mapping in `ports/`
|
|
|
|
---
|
|
|
|
## ERR-01: Sentinel Error Variables (WARNING)
|
|
|
|
Simple domain errors without context SHOULD use sentinel `var` declarations.
|
|
|
|
```go
|
|
// 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:**
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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)
|
|
|
|
```go
|
|
// 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:**
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
// 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.
|