More idiomatic than using a function (utils
is also frowned upon as a package name) is using a middleware:
func ErrorHandler(c *gin.Context) {
c.Next()
for _, err := range c.Errors {
// log, handle, etc.
}
c.JSON(http.StatusInternalServerError, "")
}
func main() {
router := gin.New()
router.Use(middleware.ErrorHandler)
// ... routes
}
Notably, you call c.Next()
inside the middleware func before your actual error handling code, so you make sure the error handling happens after the rest of the handler chain has been called.
Next should be used only inside middleware. It executes the pending handlers in the chain inside the calling handler. [...]
The advantage of using a middleware is that you can also pass arguments to it, e.g. a logger, that you may want to use later as part of the error handling, once, instead of passing it every time you call utils.ErrorHandler
directly. In this case it looks like this (I use Uber Zap loggers):
func ErrorHandler(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
for _, ginErr := range c.Errors {
logger.Error("whoops", ...)
}
}
}
func main() {
router := gin.New()
logger, _ := zap.NewDevelopment()
router.Use(middleware.ErrorHandler(logger))
// ... routes
}
The handlers then will just abort the chain, instead of calling a function, which looks cleaner and it's easier to maintain:
func (h *Handler) List(c *gin.Context) {
movies, err := h.service.ListService()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, movies)
}
It's important to note that if you set an HTTP status in c.AbortWithStatus
or c.AbortWithError
, you may want to not overwrite it in the error handler. In that case, you can call c.JSON()
with -1
as status code:
func ErrorHandler(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
for _, ginErr := range c.Errors {
logger.Error("whoops", ...)
}
// status -1 doesn't overwrite existing status code
c.JSON(-1, /* error payload */)
}
}
Lastly, using a middleware allows you to call c.Error
in your handlers multiple times, e.g. when a series of non-fatal errors occur and you want to capture all of them before actually aborting the request.
Error attaches an error to the current context. The error is pushed to a list of errors. It's a good idea to call Error for each error that occurred during the resolution of a request. A middleware can be used to collect all the errors and [process them]
func (h *Handler) List(c *gin.Context) {
err1 := /* non-fatal error */
if err1 != nil {
c.Error(err1)
}
err2 := /* another non-fatal error */
if err2 != nil {
c.Error(err2)
}
fatalErr := /* fatal error */
if fatalErr != nil {
c.AbortWithError(505, fatalErr)
return
// the error handler will have collected all 3 errors
}
c.JSON(http.StatusOK, movies)
}
As for the actual error handling in the middleware, it's pretty straightforward. Just remember that all calls to c.Error
, c.AbortWith...
will wrap your error in a gin.Error
. So to inspect the original value you have to check the err.Err
field:
func ErrorHandler(c *gin.Context) {
c.Next()
for _, err := range c.Errors {
switch err.Err {
case ErrNotFound:
c.JSON(-1, gin.H{"error": ErrNotFound.Error()})
}
// etc...
}
c.JSON(http.StatusInternalServerError, "")
}
Iterating over c.Errors
may seem unwieldy because now you have potentially N errors instead of one, but depending on how you intend to use the middleware, you can simply check len(c.Errors) > 0
and access only the first item c.Errors[0]
.