~ / Blog / Centralize HTTP Error Handling in Go
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.
The core idea is simple: change your handlers to return errors instead of handling them directly.
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...
// 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.
func (r *Router) Get(pattern string, handler HTTPHandlerWithErr) {
r.Handle("GET "+pattern, handler)
}
// Add Post, Put, Patch, Delete methods...
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.
Once you've got this pattern in place, you can:
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.