Skip to content

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 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
}
core.NotFound("user not found") // 404
core.Unauthorized("token expired") // 401
core.Forbidden("insufficient permissions") // 403
core.Conflict("email already exists") // 409
core.BadRequest("invalid input") // 400
core.Internal("database failed", err) // 500 (cause is logged, not exposed)

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
}

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
}
...
}

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
}

Define domain errors in the service layer and propagate them from handlers:

users/errors.go
var ErrUserNotFound = core.NotFound("user not found")
var ErrEmailTaken = core.Conflict("email in use")
// users/service.go
func (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.go
func (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)
}
ConstructorStatusCode
NotFound(msg)404NOT_FOUND
Unauthorized(msg)401UNAUTHORIZED
Forbidden(msg)403FORBIDDEN
Conflict(msg)409CONFLICT
BadRequest(msg)400BAD_REQUEST
Internal(msg, cause)500INTERNAL