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.ValidationErrors or validator.ValidationErrors) → 422 Unprocessable Entity JSON.
  • HTTPError → JSON with the given status code.
  • Other errors500 Internal Server Error JSON 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.