Skip to main content
Fuego provides a structured error handling system that makes it easy to return consistent error responses and handle errors gracefully throughout your application.

HTTPError Type

The foundation of Fuego’s error handling is the HTTPError type:
type HTTPError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Err     error  `json:"-"` // Underlying error (not serialized)
}
When you return an HTTPError from a handler, Fuego automatically serializes it to a consistent JSON response:
{
  "error": {
    "code": 400,
    "message": "invalid email format"
  }
}

Error Helper Functions

Fuego provides convenience functions for common HTTP errors:
// 400 Bad Request
return fuego.BadRequest("invalid input")

// 401 Unauthorized
return fuego.Unauthorized("authentication required")

// 403 Forbidden
return fuego.Forbidden("insufficient permissions")

// 404 Not Found
return fuego.NotFound("user not found")

// 409 Conflict
return fuego.Conflict("email already exists")

Using Errors in Handlers

Here’s a complete example of proper error handling in a route handler:
// app/api/users/[id]/route.go
package users

import (
    "github.com/abdul-hamid-achik/fuego/pkg/fuego"
    "myapp/internal/db"
)

func Get(c *fuego.Context) error {
    id := c.Param("id")
    if id == "" {
        return fuego.BadRequest("user ID is required")
    }

    user, err := db.FindUser(id)
    if err != nil {
        // Check if it's a not-found error
        if errors.Is(err, db.ErrNotFound) {
            return fuego.NotFound("user not found")
        }
        // Log the actual error, return generic message
        log.Printf("database error: %v", err)
        return fuego.InternalServerError("failed to fetch user")
    }

    return c.JSON(200, user)
}

func Put(c *fuego.Context) error {
    id := c.Param("id")
    
    var input struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }
    
    if err := c.Bind(&input); err != nil {
        return fuego.BadRequest("invalid JSON body")
    }

    // Validate input
    if input.Name == "" {
        return fuego.BadRequest("name is required")
    }
    if !isValidEmail(input.Email) {
        return fuego.BadRequest("invalid email format")
    }

    // Check for conflicts
    existing, _ := db.FindUserByEmail(input.Email)
    if existing != nil && existing.ID != id {
        return fuego.Conflict("email already in use")
    }

    user, err := db.UpdateUser(id, input)
    if err != nil {
        return fuego.InternalServerError("failed to update user")
    }

    return c.JSON(200, user)
}

Wrapping Errors with Context

Use WrapError to add context to errors while preserving the original:
func Get(c *fuego.Context) error {
    data, err := fetchFromExternalAPI()
    if err != nil {
        // Wrap with context for logging
        wrappedErr := fuego.WrapError(err, "failed to fetch external data")
        log.Printf("error: %v", wrappedErr)
        
        return fuego.InternalServerError("external service unavailable")
    }
    return c.JSON(200, data)
}

Errors with Underlying Causes

Create errors with underlying causes for debugging:
func Get(c *fuego.Context) error {
    user, err := db.FindUser(id)
    if err != nil {
        // Create error with cause (cause is logged but not sent to client)
        return fuego.NewHTTPErrorWithCause(
            500,
            "failed to fetch user",
            err, // Original error preserved for logging
        )
    }
    return c.JSON(200, user)
}

Checking Error Types

Use IsHTTPError to check if an error is an HTTPError:
func handleError(err error) {
    if httpErr, ok := fuego.IsHTTPError(err); ok {
        // Handle HTTPError specifically
        log.Printf("HTTP %d: %s", httpErr.Code, httpErr.Message)
        
        // Access underlying cause if present
        if httpErr.Err != nil {
            log.Printf("Caused by: %v", httpErr.Err)
        }
    } else {
        // Handle other errors
        log.Printf("Unexpected error: %v", err)
    }
}

Error Handling in Middleware

Middleware can catch and transform errors:
// app/api/middleware.go
package api

import "github.com/abdul-hamid-achik/fuego/pkg/fuego"

func Middleware() fuego.MiddlewareFunc {
    return func(next fuego.HandlerFunc) fuego.HandlerFunc {
        return func(c *fuego.Context) error {
            err := next(c)
            
            if err != nil {
                // Log all errors
                log.Printf("[%s %s] error: %v", c.Method(), c.Path(), err)
                
                // Transform panic recovery errors
                if httpErr, ok := fuego.IsHTTPError(err); ok {
                    // Add request ID to error response
                    return c.JSON(httpErr.Code, map[string]any{
                        "error": map[string]any{
                            "code":       httpErr.Code,
                            "message":    httpErr.Message,
                            "request_id": c.Header("X-Request-Id"),
                        },
                    })
                }
                
                // Unknown errors become 500
                return c.JSON(500, map[string]any{
                    "error": map[string]any{
                        "code":    500,
                        "message": "internal server error",
                    },
                })
            }
            
            return nil
        }
    }
}

The Recover Middleware

Always use the Recover middleware in production to catch panics:
// main.go
app := fuego.New()
app.Use(fuego.Recover()) // Catches panics, returns 500
Without Recover middleware, a panic in your handler will crash the entire server. Always add it as your first middleware.

Validation Patterns

Struct Validation

type CreateUserInput struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

func (i *CreateUserInput) Validate() error {
    if i.Name == "" {
        return fuego.BadRequest("name is required")
    }
    if len(i.Name) < 2 {
        return fuego.BadRequest("name must be at least 2 characters")
    }
    if !isValidEmail(i.Email) {
        return fuego.BadRequest("invalid email format")
    }
    if len(i.Password) < 8 {
        return fuego.BadRequest("password must be at least 8 characters")
    }
    return nil
}

func Post(c *fuego.Context) error {
    var input CreateUserInput
    if err := c.Bind(&input); err != nil {
        return fuego.BadRequest("invalid JSON body")
    }
    
    if err := input.Validate(); err != nil {
        return err // Already an HTTPError
    }
    
    // Create user...
    return c.JSON(201, user)
}

Multiple Validation Errors

For forms with multiple fields, collect all errors:
type ValidationErrors struct {
    Errors map[string]string `json:"errors"`
}

func validateInput(input CreateUserInput) *ValidationErrors {
    errors := make(map[string]string)
    
    if input.Name == "" {
        errors["name"] = "name is required"
    }
    if !isValidEmail(input.Email) {
        errors["email"] = "invalid email format"
    }
    if len(input.Password) < 8 {
        errors["password"] = "must be at least 8 characters"
    }
    
    if len(errors) > 0 {
        return &ValidationErrors{Errors: errors}
    }
    return nil
}

func Post(c *fuego.Context) error {
    var input CreateUserInput
    if err := c.Bind(&input); err != nil {
        return fuego.BadRequest("invalid JSON body")
    }
    
    if validationErrors := validateInput(input); validationErrors != nil {
        return c.JSON(422, validationErrors)
    }
    
    // Create user...
    return c.JSON(201, user)
}

Error Response Formats

{
  "error": {
    "code": 400,
    "message": "invalid email format"
  }
}
{
  "errors": {
    "name": "name is required",
    "email": "invalid email format",
    "password": "must be at least 8 characters"
  }
}
{
  "error": {
    "code": 404,
    "message": "user not found",
    "request_id": "abc123",
    "documentation_url": "https://api.example.com/docs/errors#not-found"
  }
}

Best Practices

Log the actual error for debugging, but return a generic message to clients:
if err != nil {
    log.Printf("database error: %v", err) // Log the real error
    return fuego.InternalServerError("failed to process request") // Generic message
}
SituationStatus CodeHelper Function
Invalid input400BadRequest()
Not authenticated401Unauthorized()
Authenticated but not allowed403Forbidden()
Resource doesn’t exist404NotFound()
Duplicate resource409Conflict()
Server error500InternalServerError()
Use the same error response structure throughout your API. Consider creating a helper:
func errorResponse(code int, message string) map[string]any {
    return map[string]any{
        "error": map[string]any{
            "code":    code,
            "message": message,
        },
    }
}
Include request IDs and timestamps in error logs:
log.Printf("[%s] [%s %s] error: %v",
    time.Now().Format(time.RFC3339),
    c.Method(),
    c.Path(),
    err,
)
The error helpers (BadRequest, NotFound, etc.) return *HTTPError which implements the error interface, so you can return them directly from handlers.

Next Steps