In this short post, I'll share with you a simple pattern I use to centralize error handling for my HTTP handlers.
If you've written any amount of Go HTTP servers, you've probably gotten tired of writing the same error handling code over and over again:
func SomeHandler(w http.ResponseWriter, r *http.Request) {
data, err := fetchSomeData()
if err != nil {
http.Error(w, "Failed to fetch data", http.StatusInternalServerError)
log.Printf("Error fetching data: %v", err)
return
}
// More if-err blocks...
}
This code is repetitive, and clutters your handlers with boilerplate instead of business logic.
A Better Way
The core idea is simple: change your handlers to return errors instead of handling them directly.
Step 1: Define Custom HTTP Errors
package httperror
import (
"errors"
"net/http"
)
type HTTPError struct {
error
Code int
}
func New(code int, message string) *HTTPError {
return &HTTPError{
error: errors.New(message),
Code: code,
}
}
func NotFound(message string) *HTTPError {
return New(http.StatusNotFound, message)
}
// Add more helpers as needed...
Step 2: Create a Handler Wrapper
// Define a new handler type that returns an error
type HTTPHandlerWithErr func(http.ResponseWriter, *http.Request) error
// Handle wraps your error-returning handlers
func (r *Router) Handle(pattern string, handler HTTPHandlerWithErr) {
r.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
if err := handler(w, r); err != nil {
// Check if it's an HTTPError
var httpErr *httperror.HTTPError
if errors.As(err, &httpErr) {
http.Error(w, err.Error(), httpErr.Code)
slog.Debug("http error", "code", httpErr.Code, "err", err.Error())
} else {
// Default to 500
http.Error(w, err.Error(), http.StatusInternalServerError)
slog.Error("internal server error", "err", err.Error())
}
}
})
}
This wrapper does all the error handling heavy lifting. It uses errors.As()
to check if the error is an HTTPError
and extract the status code.
Step 3: Add Method-Specific Helpers
func (r *Router) Get(pattern string, handler HTTPHandlerWithErr) {
r.Handle("GET "+pattern, handler)
}
// Add Post, Put, Patch, Delete methods...
Step 4: Write Clean Handlers
func (c *ContainersController) Show(w http.ResponseWriter, r *http.Request) error {
id := r.PathValue("id")
container, err := c.service.FindContainer(id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return httperror.NotFound("container not found")
}
return err
}
return json.NewEncoder(w).Encode(container)
}
Look how clean this handler is now! It focuses on its job instead of error handling gymnastics.
What's Next?
Once you've got this pattern in place, you can:
- Use JSON responses: Return errors as JSON for API endpoints
- Add request IDs: Thread request IDs through logs and responses
- Build error-aware middleware: Create middleware that works with your error-returning handlers
- Improve error pages: Replace plain text errors with proper error pages
This pattern works with any router that accepts standard Go handlers. It's a small change that makes a huge difference in code quality and maintainability.