Skip to content

Error Handling

ss-keel-core provides a built-in error type KError that maps directly to HTTP status codes. Any *KError returned from a handler is automatically serialized to a JSON error response.

KError carries a status code, a machine-readable code string, 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("db failed", err) // 500 (cause logged, not exposed)

Return a *KError directly from your handler:

func (c *UserController) getByID(ctx *core.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 via errors.As and returns:

{
"code": "NOT_FOUND",
"message": "user not found",
"statusCode": 404
}

ParseBody automatically returns a 422 Unprocessable Entity with field-level 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 — returning the error from ParseBody is enough:

func (c *UserController) create(ctx *core.Ctx) error {
var dto CreateUserDTO
if err := ctx.ParseBody(&dto); err != nil {
return err // 400 or 422 automatically
}
...
}

Use core.Internal when an unexpected error occurs so the cause is logged but not leaked to the client:

result, err := db.Query(...)
if err != nil {
return core.Internal("failed to query users", err)
// Response: 500 Internal Server Error
// The original error is logged internally
}

Define domain-level errors in your service layer and return them from handlers:

users/errors.go
var ErrUserNotFound = core.NotFound("user not found")
var ErrEmailTaken = core.Conflict("email already 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 *core.Ctx) error {
user, err := c.service.GetByID(ctx.Context(), ctx.Params("id"))
if err != nil {
return err // KError propagates 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