Error Handling Guide
Rocco uses typed sentinel errors for consistent, documented error responses. This guide covers error patterns, custom errors, and best practices.
Error Response Format
All errors return a consistent JSON structure:
{
"code": "NOT_FOUND",
"message": "user not found",
"details": {
"resource": "user"
}
}
| Field | Description |
|---|---|
code | Machine-readable error code (e.g., NOT_FOUND) |
message | Human-readable message |
details | Optional structured details (varies by error type) |
Built-in Errors
Rocco provides typed errors for common HTTP status codes:
| Error | Status | Code | Details Type |
|---|---|---|---|
ErrBadRequest | 400 | BAD_REQUEST | BadRequestDetails |
ErrUnauthorized | 401 | UNAUTHORIZED | UnauthorizedDetails |
ErrForbidden | 403 | FORBIDDEN | ForbiddenDetails |
ErrNotFound | 404 | NOT_FOUND | NotFoundDetails |
ErrConflict | 409 | CONFLICT | ConflictDetails |
ErrPayloadTooLarge | 413 | PAYLOAD_TOO_LARGE | PayloadTooLargeDetails |
ErrUnprocessableEntity | 422 | UNPROCESSABLE_ENTITY | UnprocessableEntityDetails |
ErrValidationFailed | 422 | VALIDATION_FAILED | ValidationDetails |
ErrTooManyRequests | 429 | TOO_MANY_REQUESTS | TooManyRequestsDetails |
ErrInternalServer | 500 | INTERNAL_SERVER_ERROR | InternalServerDetails |
ErrNotImplemented | 501 | NOT_IMPLEMENTED | NotImplementedDetails |
ErrServiceUnavailable | 503 | SERVICE_UNAVAILABLE | ServiceUnavailableDetails |
Using Errors
Simple Error Return
func(req *rocco.Request[rocco.NoBody]) (User, error) {
user, err := db.FindUser(req.Params.Path["id"])
if err != nil {
return User{}, rocco.ErrNotFound
}
return user, nil
}
With Custom Message
return User{}, rocco.ErrNotFound.WithMessage("user not found")
With Typed Details
return User{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{
Resource: "user",
})
Response:
{
"code": "NOT_FOUND",
"message": "not found",
"details": {
"resource": "user"
}
}
Combining Message and Details
return User{}, rocco.ErrNotFound.
WithMessage("user not found").
WithDetails(rocco.NotFoundDetails{Resource: "user"})
Error Declaration
All sentinel errors must be declared with WithErrors():
handler := rocco.NewHandler[rocco.NoBody, User](
"get-user",
"GET",
"/users/{id}",
getUser,
).WithErrors(rocco.ErrNotFound, rocco.ErrForbidden)
Why Declaration Matters
- OpenAPI Generation: Declared errors appear in the API spec with proper schemas
- Runtime Safety: Undeclared errors return 500 to prevent information leakage
- Documentation: Consumers know which errors to expect
Undeclared Error Behavior
If a handler returns an undeclared sentinel error:
- Rocco logs a warning with the error details
- Returns 500 Internal Server Error to the client
- The original error code is hidden
This catches programming errors where handlers return errors they didn't declare.
Custom Errors
Define domain-specific errors with typed details:
Step 1: Define Details Type
type InsufficientFundsDetails struct {
Required float64 `json:"required" description:"Amount required"`
Available float64 `json:"available" description:"Amount available"`
Currency string `json:"currency" description:"Currency code"`
}
Step 2: Create Error
var ErrInsufficientFunds = rocco.NewError[InsufficientFundsDetails](
"INSUFFICIENT_FUNDS", // Code
402, // Status
"insufficient funds", // Default message
)
Step 3: Use in Handler
handler := rocco.NewHandler[TransferInput, TransferResult](
"transfer-funds",
"POST",
"/transfers",
func(req *rocco.Request[TransferInput]) (TransferResult, error) {
balance := getBalance(req.Body.FromAccount)
if balance < req.Body.Amount {
return TransferResult{}, ErrInsufficientFunds.WithDetails(InsufficientFundsDetails{
Required: req.Body.Amount,
Available: balance,
Currency: "USD",
})
}
return executeTransfer(req.Body)
},
).WithErrors(ErrInsufficientFunds, rocco.ErrNotFound)
Response:
{
"code": "INSUFFICIENT_FUNDS",
"message": "insufficient funds",
"details": {
"required": 100.00,
"available": 50.00,
"currency": "USD"
}
}
Error Wrapping
Wrap underlying errors for debugging:
return User{}, rocco.ErrInternalServer.WithCause(err)
The cause is logged but not exposed to clients.
Validation Errors
Validation failures automatically return structured errors:
{
"code": "VALIDATION_FAILED",
"message": "validation failed",
"details": {
"fields": [
{"field": "Email", "tag": "email", "value": "not-an-email"},
{"field": "Name", "tag": "min", "value": "A"}
]
}
}
These are generated automatically from validate struct tags.
Error Patterns
Resource Not Found
user, err := db.FindUser(id)
if errors.Is(err, sql.ErrNoRows) {
return User{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{
Resource: "user",
})
}
if err != nil {
return User{}, rocco.ErrInternalServer.WithCause(err)
}
Conflict Detection
_, err := db.CreateUser(input)
if isUniqueViolation(err) {
return User{}, rocco.ErrConflict.
WithMessage("email already registered").
WithDetails(rocco.ConflictDetails{
Reason: "a user with this email already exists",
})
}
Authorization Errors
if !req.Identity.HasScope("users:write") {
return User{}, rocco.ErrForbidden.WithMessage("requires users:write scope")
}
Multiple Error Conditions
func(req *rocco.Request[UpdateUserInput]) (User, error) {
user, err := db.FindUser(req.Params.Path["id"])
if errors.Is(err, sql.ErrNoRows) {
return User{}, rocco.ErrNotFound
}
if err != nil {
return User{}, rocco.ErrInternalServer.WithCause(err)
}
if user.TenantID != req.Identity.TenantID() {
return User{}, rocco.ErrForbidden.WithMessage("cannot access other tenant's users")
}
updated, err := db.UpdateUser(user.ID, req.Body)
if isUniqueViolation(err) {
return User{}, rocco.ErrConflict.
WithMessage("email already in use").
WithDetails(rocco.ConflictDetails{
Reason: "this email is already registered to another user",
})
}
return updated, nil
}
Best Practices
1. Declare All Errors
Always declare every error a handler may return:
handler.WithErrors(
rocco.ErrNotFound,
rocco.ErrConflict,
rocco.ErrForbidden,
)
2. Use Specific Error Types
// GOOD - specific error
return User{}, rocco.ErrNotFound
// AVOID - generic error
return User{}, errors.New("not found")
3. Add Context with Details
// GOOD - useful context
return User{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{
Resource: "user",
})
// LESS USEFUL - no context
return User{}, rocco.ErrNotFound
4. Hide Internal Errors
// GOOD - hide internal details
if err != nil {
return User{}, rocco.ErrInternalServer.WithCause(err)
}
// BAD - exposes internal error
return User{}, err
5. Domain-Specific Errors
Create custom errors for domain concepts:
var (
ErrOrderCancelled = rocco.NewError[OrderCancelledDetails](
"ORDER_CANCELLED", 400, "order already cancelled")
ErrInventoryLow = rocco.NewError[InventoryDetails](
"INVENTORY_LOW", 409, "insufficient inventory")
)
See Also
- API Reference - Errors - Complete error reference
- Handler Guide - Handler configuration
- Best Practices - Production patterns