Error Handling
ss-keel-core includes a KError error type that maps directly to HTTP status codes. Any *KError returned from a handler is automatically serialized as a JSON response.
KError
Section titled “KError”KError contains a status code, a machine-readable code, and a human-readable message.
type KError struct { Code string StatusCode int Message string Cause error // optional, not exposed in responses}Built-in constructors
Section titled “Built-in constructors”core.NotFound("user not found") // 404core.Unauthorized("token expired") // 401core.Forbidden("insufficient permissions") // 403core.Conflict("email already exists") // 409core.BadRequest("invalid input") // 400core.Internal("database failed", err) // 500 (cause is logged, not exposed)Returning errors in handlers
Section titled “Returning errors in handlers”Return a *KError directly from your handler:
func (c *UserController) getByID(ctx *httpx.Ctx) error { id := ctx.Params("id")
user, err := c.service.GetByID(ctx.Context(), id) if err != nil { return core.NotFound("user not found") }
return ctx.OK(user)}The framework’s error handler detects *KError using errors.As and responds:
{ "code": "NOT_FOUND", "message": "user not found", "statusCode": 404}Validation errors
Section titled “Validation errors”ParseBody automatically returns 422 Unprocessable Entity with per-field errors when validation fails:
{ "errors": [ { "field": "email", "message": "must be a valid email" }, { "field": "name", "message": "this field is required" } ]}You don’t need to handle this manually: just return the error from ParseBody.
func (c *UserController) create(ctx *httpx.Ctx) error { var dto CreateUserDTO if err := ctx.ParseBody(&dto); err != nil { return err // automatic 400 or 422 } ...}Wrapping errors
Section titled “Wrapping errors”Use core.Internal when an unexpected error occurs so it gets logged internally without leaking details to the client:
result, err := db.Query(...)if err != nil { return core.Internal("user query failed", err) // Response: 500 Internal Server Error // The original error is logged internally}Propagating errors across layers
Section titled “Propagating errors across layers”Define domain errors in the service layer and propagate them from handlers:
var ErrUserNotFound = core.NotFound("user not found")var ErrEmailTaken = core.Conflict("email in use")
// users/service.gofunc (s *UserService) GetByID(ctx context.Context, id string) (*User, error) { user, err := s.repo.FindByID(ctx, id) if err != nil { return nil, ErrUserNotFound } return user, nil}
// users/controller.gofunc (c *UserController) getByID(ctx *httpx.Ctx) error { user, err := c.service.GetByID(ctx.Context(), ctx.Params("id")) if err != nil { return err // KError bubbles up to the error handler } return ctx.OK(user)}Error response format
Section titled “Error response format”| Constructor | Status | Code |
|---|---|---|
NotFound(msg) | 404 | NOT_FOUND |
Unauthorized(msg) | 401 | UNAUTHORIZED |
Forbidden(msg) | 403 | FORBIDDEN |
Conflict(msg) | 409 | CONFLICT |
BadRequest(msg) | 400 | BAD_REQUEST |
Internal(msg, cause) | 500 | INTERNAL |