Error Model
Error Model
Section titled “Error Model”Every Plumego handler returns errors using the same two functions: contract.WriteError and contract.NewErrorBuilder. This single path means every error response has a consistent JSON shape that clients can parse reliably.
The wire format
Section titled “The wire format”{ "error": { "type": "required_field_missing", "code": "REQUIRED", "message": "name is required", "category": "validation_error", "details": { "field": "name" } }, "request_id": "0jx9f3kp2q"}request_id is injected automatically when middleware/requestid has run. You never set it manually.
Building an error
Section titled “Building an error”Use NewErrorBuilder — never construct APIError as a struct literal:
import "github.com/spcent/plumego/contract"
err := contract.NewErrorBuilder(). Type(contract.TypeRequired). // sets type, code, category, HTTP status Detail("field", "name"). // optional: adds to the details map Message("name is required"). // overrides the default human-readable text Build()
contract.WriteError(w, r, err)Calling .Type() is the only required step. It populates type, code, category, and the HTTP status code automatically. You only override Message and Detail when the default text is insufficient.
ErrorType catalog
Section titled “ErrorType catalog”| Constant | HTTP | code | category |
|---|---|---|---|
TypeValidation | 400 | VALIDATION_ERROR | validation_error |
TypeRequired | 400 | REQUIRED_FIELD_MISSING | validation_error |
TypeInvalidFormat | 400 | INVALID_FORMAT | validation_error |
TypeOutOfRange | 400 | VALUE_OUT_OF_RANGE | validation_error |
TypeDuplicate | 400 | DUPLICATE_VALUE | validation_error |
TypeUnauthorized | 401 | UNAUTHORIZED | auth_error |
TypeInvalidToken | 401 | INVALID_TOKEN | auth_error |
TypeExpiredToken | 401 | EXPIRED_TOKEN | auth_error |
TypeForbidden | 403 | FORBIDDEN | auth_error |
TypeNotFound | 404 | RESOURCE_NOT_FOUND | client_error |
TypeConflict | 409 | RESOURCE_CONFLICT | client_error |
TypeAlreadyExists | 409 | RESOURCE_ALREADY_EXISTS | client_error |
TypeGone | 410 | RESOURCE_GONE | client_error |
TypeRateLimited | 429 | RATE_LIMITED | rate_limit_error |
TypeInternal | 500 | INTERNAL_ERROR | server_error |
TypeUnavailable | 503 | SERVICE_UNAVAILABLE | server_error |
TypeTimeout | 408 | TIMEOUT | timeout_error |
TypeMethodNotAllowed | 405 | METHOD_NOT_ALLOWED | client_error |
TypeNotImplemented | 501 | NOT_IMPLEMENTED | server_error |
TypeBadGateway | 502 | BAD_GATEWAY | server_error |
TypeGatewayTimeout | 504 | GATEWAY_TIMEOUT | timeout_error |
TypeMaintenance | 503 | MAINTENANCE_MODE | server_error |
ErrorCategory
Section titled “ErrorCategory”The category field groups errors for observability and alerting:
| Value | Meaning |
|---|---|
client_error | 4xx — bad client input |
server_error | 5xx — infrastructure or server logic failure |
validation_error | Input validation failure (subset of client_error) |
auth_error | Authentication or authorization failure |
rate_limit_error | Rate limit exceeded |
timeout_error | Timeout |
Handler pattern
Section titled “Handler pattern”func (h ItemHandler) Create(w http.ResponseWriter, r *http.Request) { var req CreateItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeValidation). Message(err.Error()). Build()) return } if req.Name == "" { contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeRequired). Detail("field", "name"). Build()) return }
item, err := h.svc.Create(r.Context(), req) if errors.Is(err, ErrAlreadyExists) { contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeAlreadyExists). Message("item with this name already exists"). Build()) return } if err != nil { contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeInternal). Build()) return }
contract.WriteResponse(w, r, http.StatusCreated, item, nil)}Multiple validation errors
Section titled “Multiple validation errors”When you need to return multiple field errors at once, use the Details map to carry them:
errs := map[string]string{}if req.Name == "" { errs["name"] = "required" }if req.Email == "" { errs["email"] = "required" }
if len(errs) > 0 { b := contract.NewErrorBuilder(). Type(contract.TypeValidation). Message("request validation failed") for field, msg := range errs { b = b.Detail(field, msg) } contract.WriteError(w, r, b.Build()) return}