This content originally appeared on DEV Community and was authored by Harry Douglas
When building an API in Go, you may notice you keep writing this kind of code everywhere:
if email == "" {
validationError := responses.Response{
Code: 422,
Message: "Invalid email",
}
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(validationError)
return
}
This gets repetitive, messy, and hard to manage as your project grows.
If you’re coming from Laravel, you’re used to something like throw new Exception(...)
, and having Laravel automatically return a proper JSON response.
In Go, you can achieve the same thing using:
- A custom error type
- Middleware
-
panic()
+recover()
Let’s build it together
Goal
- Stop repeating the same error-handling code
- Throw errors in one line
- Always return a clean JSON response
- Centralize the logic in one place
Step 1: Define a Common Error Response Format
Create a file like responses/Response.go
:
package responses
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
}
This is what your API will return to the client.
Step 2: Create Custom Error Types
In exceptions/ApiError.go
, define your custom error structure:
package exceptions
import "net/http"
type APIError struct {
Code int
Message string
}
func (e APIError) Error() string {
return e.Message
}
Also add helper functions for common error types:
func NotFound(message string) APIError {
return APIError{Code: http.StatusNotFound, Message: message}
}
func Unprocessable(message string) APIError {
return APIError{Code: http.StatusUnprocessableEntity, Message: message}
}
Now, you can easily throw:
panic(exceptions.Unprocessable("Email is required"))
Step 3: Global Middleware to Catch Panics and Return JSON
Create a recovery middleware:
package middleware
import (
"encoding/json"
"net/http"
"yourapp/exceptions"
"yourapp/responses"
)
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
code := http.StatusInternalServerError
message := "Internal Server Error"
if apiErr, ok := rec.(exceptions.APIError); ok {
code = apiErr.Code
message = apiErr.Message
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(responses.ErrorResponse{
Code: code,
Message: message,
})
}
}()
next.ServeHTTP(w, r)
})
}
Now, all panics will be caught and turned into clean JSON.
Step 4: Use the Middleware in main.go
package main
import (
"net/http"
"yourapp/handlers/auth"
"yourapp/middleware"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/auth/email", auth.EmailHandler)
http.ListenAndServe(":8080", middleware.RecoveryMiddleware(mux))
}
Step 5: Throw Errors in Your Handler Instead of Writing Response Code
Old way (repetitive):
if email == "" {
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(responses.ErrorResponse{
Code: 422,
Message: "Email is required",
})
return
}
New way (cleaner):
if email == "" {
panic(exceptions.Unprocessable("Email is required"))
}
That’s it!
Example Output
If the user submits an empty email, they will get:
{
"code": 422,
"message": "Email is required"
}
This approach gives you Laravel-style centralized error handling in Go.
Instead of repeating this everywhere:
w.WriteHeader(...)
json.NewEncoder(w).Encode(...)
You just call:
panic(exceptions.Unprocessable("message"))
And let your middleware handle the rest. Clean, consistent, and scalable.
This content originally appeared on DEV Community and was authored by Harry Douglas