Error Handling
Custom error types
Section titled “Custom error types”Use WithError to declare that a route may return a specific error type at a given HTTP status code. The error type must implement the error interface — its struct fields are reflected into the OpenAPI schema automatically.
Define an error type
Section titled “Define an error type”type ConflictError struct { Code string `json:"code"` Message string `json:"message"`}
func (e *ConflictError) Error() string { return e.Message }Register it on a route
Section titled “Register it on a route”shiftapi.Handle(api, "POST /users", createUser, shiftapi.WithError[*ConflictError](http.StatusConflict), shiftapi.WithError[*NotFoundError](http.StatusNotFound),)Return it from the handler
Section titled “Return it from the handler”func createUser(r *http.Request, in *CreateUser) (*User, error) { if exists { return nil, &ConflictError{ Code: "EMAIL_TAKEN", Message: "a user with that email already exists", } } return user, nil}This returns:
HTTP/1.1 409 Conflict
{"code": "EMAIL_TAKEN", "message": "a user with that email already exists"}At runtime, ShiftAPI checks if the returned error matches a registered type using errors.As. Wrapped errors work automatically:
return nil, fmt.Errorf("db lookup failed: %w", &ConflictError{Code: "EMAIL_TAKEN", Message: "conflict"})Multiple error types
Section titled “Multiple error types”A single route can declare multiple error types:
shiftapi.Handle(api, "POST /users", createUser, shiftapi.WithError[*ConflictError](http.StatusConflict), shiftapi.WithError[*NotFoundError](http.StatusNotFound), shiftapi.WithError[*ForbiddenError](http.StatusForbidden),)Each declared error type appears as a separate response in the generated OpenAPI spec with its own schema.
Scoping errors
Section titled “Scoping errors”WithError is an Option that works at all three levels:
// API level — applies to all routesapi := shiftapi.New( shiftapi.WithError[*AuthError](http.StatusUnauthorized),)
// Group level — applies to all routes in the groupv1 := api.Group("/api/v1", shiftapi.WithError[*RateLimitError](http.StatusTooManyRequests),)
// Route level — applies to this route onlyshiftapi.Handle(v1, "GET /users/{id}", getUser, shiftapi.WithError[*NotFoundError](http.StatusNotFound),)Error types merge across all three levels. In the example above, GET /api/v1/users/{id} has AuthError (from API), RateLimitError (from group), and NotFoundError (from route).
Parse errors
Section titled “Parse errors”When the framework cannot parse the request (malformed JSON, invalid query parameter types, invalid header values, invalid form data), it returns a 400 Bad Request. By default the response body is {"message": "bad request"}.
Customizing the 400 response
Section titled “Customizing the 400 response”Use WithBadRequestError to customize the 400 response body. The function receives the parse error so you can decide what to expose to the client:
api := shiftapi.New( shiftapi.WithBadRequestError(func(err error) *MyBadRequest { return &MyBadRequest{Code: "BAD_REQUEST", Message: err.Error()} }),)The return type determines the BadRequestError schema in the OpenAPI spec.
Validation errors
Section titled “Validation errors”When request validation fails, ShiftAPI automatically returns a 422 Unprocessable Entity with per-field error details:
HTTP/1.1 422 Unprocessable Entity
{ "message": "validation failed", "errors": [ { "field": "Email", "message": "must be a valid email address" } ]}Unhandled errors
Section titled “Unhandled errors”If a handler returns an error that doesn’t match ValidationError or any type registered with WithError, ShiftAPI returns a 500 Internal Server Error with a generic message. The original error is not exposed to the client.
Customizing the 500 response
Section titled “Customizing the 500 response”Use WithInternalServerError to customize the 500 response body. The function receives the unhandled error so you can log it or extract details:
api := shiftapi.New( shiftapi.WithInternalServerError(func(err error) *MyServerError { log.Error("unhandled", "err", err) return &MyServerError{Code: "INTERNAL_ERROR", Message: "internal server error"} }),)The return type determines the InternalServerError schema in the OpenAPI spec.
OpenAPI spec
Section titled “OpenAPI spec”Every route in the generated OpenAPI spec includes:
400—BadRequestErrorschema (default:messageonly, customizable viaWithBadRequestError)422—ValidationErrorschema (message+errorsarray)500—InternalServerErrorschema (default:messageonly, customizable viaWithInternalServerError)- Any additional status codes declared via
WithError, each with the custom error type’s schema
These schemas are registered as reusable components in #/components/schemas/ and referenced via $ref.