Exception Handling
Nimbus handlers return an error. When a handler returns a non-nil error, the framework's global error handler converts it into an HTTP response. You can customise this behaviour with custom error types, middleware, and panic recovery.
Returning errors from handlers
Every handler has the signature func(*http.Context) error. Return nil on success or an error to trigger error handling.
func getUser(c *http.Context) error {
id := c.Param("id")
user, err := findUser(id)
if err != nil {
return err // triggers 500 by default
}
return c.JSON(200, user)
}
Global error handler (core)
Nimbus ships with a global error handler in errors.Handler(). It is wired into the starter app's start/kernel.go:
// File: start/kernel.go
app.Router.Use(
middleware.Logger(),
middleware.Recover(),
errors.Handler(),
// ...
)
Behaviour:
- Validation errors (
validation.ValidationErrorsorvalidator.ValidationErrors) →422 Unprocessable EntityJSON. - HTTPError → JSON with the given status code.
- Other errors →
500 Internal Server ErrorJSON with a generic message.
HTTPError helper
Use errors.HTTPError to return HTTP errors from handlers:
// File: app/controllers/users.go
import "github.com/CodeSyncr/nimbus/errors"
func Show(c *http.Context) error {
id := c.Param("id")
user, err := findUser(id)
if err == sql.ErrNoRows {
return errors.HTTPError{
Status: http.StatusNotFound,
Message: "User not found",
}
}
if err != nil {
return err // 500
}
return c.JSON(http.StatusOK, user)
}
Validation errors (422)
Combine validation helpers with the global handler for consistent 422 responses:
type CreatePostPayload struct {
Title string `validate:"required,min=3"`
Body string `validate:"required"`
}
func CreatePost(c *http.Context) error {
var payload CreatePostPayload
if err := validation.ValidateRequestJSON(c.Request.Body, &payload); err != nil {
// errors.Handler will detect validator errors and return 422 JSON:
// { "Title": ["required"], "Body": ["required"] }
return err
}
// ...
return c.JSON(http.StatusCreated, post)
}
Form requests + error handling
Form requests wrap validation + authorization in a reusable type. They integrate seamlessly with the global error handler:
type LoginPayload struct {
Email string `validate:"required,email"`
Password string `validate:"required"`
}
type LoginRequest struct {
validation.BaseFormRequest[LoginPayload]
}
func (r *LoginRequest) Payload() *LoginPayload {
return &LoginPayload{}
}
func (r *LoginRequest) Authorize(c *http.Context) error {
// Allow all for this example.
return nil
}
func Login(c *http.Context) error {
req := &LoginRequest{}
payload, ve, err := validation.BindAndValidate(c, req)
if ve != nil {
// Handler will turn this into 422 JSON automatically.
return ve
}
if err != nil {
return err
}
// use payload.Email, payload.Password ...
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
}
Error IDs (AppError)
Every error gets a unique ID for tracking. The ID is logged server-side while the client only sees a safe message and the ID for support reference.
import "github.com/CodeSyncr/nimbus/errors"
func Show(c *http.Context) error {
user, err := findUser(c.Param("id"))
if err != nil {
// Creates an error with unique ID (e.g. "a1b2c3d4e5f6")
return errors.Wrap(500, err)
}
return c.JSON(200, user)
}
// Client receives:
// {"error": "Internal Server Error", "error_id": "a1b2c3d4e5f6"}
// Server logs:
// error_id=a1b2c3d4e5f6 status=500 database timeout
You can also create errors directly:
// New creates an AppError with a unique tracking ID
appErr := errors.New(503, "service unavailable")
Error reporters
Register external error reporting services (Sentry, Bugsnag, etc.) to automatically receive errors:
import "github.com/CodeSyncr/nimbus/errors"
// Built-in log reporter (logs to nimbus logger)
errors.RegisterReporter(&errors.LogReporter{})
// Custom reporter (e.g. Sentry)
type SentryReporter struct{}
func (s *SentryReporter) Report(err error, ctx map[string]any) error {
sentry.CaptureException(err)
return nil
}
errors.RegisterReporter(&SentryReporter{})
// Reports are sent automatically when errors.Handler() catches an AppError.
// You can also report manually:
errors.ReportError(err, map[string]any{"user_id": 42})
Panic recovery
The built-in middleware.Recover() catches panics inside handlers, logs them, and returns a 500 JSON response instead of crashing the server.
app.Router.Use(middleware.Recover())
func riskyHandler(c *http.Context) error {
// If this panics, Recover() catches it and returns 500
result := doSomethingDangerous()
return c.JSON(200, result)
}
Always register Recover() early in the middleware chain — typically as the first or second middleware — so it catches panics from all downstream handlers and middleware.
Logging errors
Use the Nimbus logger package for structured error logging inside your error handler or handlers.
import "github.com/CodeSyncr/nimbus/logger"
func ErrorHandlerWithLogging() router.Middleware {
return func(next router.HandlerFunc) router.HandlerFunc {
return func(c *http.Context) error {
err := next(c)
if err != nil {
logger.Error("request failed",
"method", c.Request.Method,
"path", c.Request.URL.Path,
"error", err.Error(),
)
return c.JSON(500, map[string]string{
"error": "internal server error",
})
}
return nil
}
}
}
Recommended middleware order
Register error-related middleware in this order for correct behaviour:
app.Router.Use(
middleware.Recover(), // 1. catch panics first
middleware.Logger(), // 2. log all requests
ErrorHandler(), // 3. convert errors to responses
)
Recover()is outermost so it catches panics from everything inside.Logger()logs every request regardless of outcome.errors.Handler()converts returned errors into proper JSON responses.