gin: Question: Handling errors

I’m in the process of writing a REST API which returns JSON. I’ve found my route handlers can get quite cluttered with error handling. I’ve been looking at ways I can reduce and simply error handling and would appreciate anyone’s opinions or advice on what I’ve come up with.

I’m following the JSON API standard for error returning, http://jsonapi.org/format/#errors, they contain a bit more than the standard Error in Go, so when I initiate an error it can get quite big.

I have created an APIError struct, some helper functions and some default errors:

type APIErrors struct {
    Errors      []*APIError `json:"errors"`
}

func (errors *APIErrors) Status() int {
    return errors.Errors[0].Status
}

type APIError struct {
    Status      int         `json:"status"`
    Code        string      `json:"code"`
    Title       string      `json:"title"`
    Details     string      `json:"details"`
    Href        string      `json:"href"`
}

func newAPIError(status int, code string, title string, details string, href string) *APIError {
    return &APIError{
        Status:     status,
        Code:       code,
        Title:      title,
        Details:    details,
        Href:       href,
    }
}

var (
    errDatabase         = newAPIError(500, "database_error", "Database Error", "An unknown error occurred.", "")
    errInvalidSet       = newAPIError(404, "invalid_set", "Invalid Set", "The set you requested does not exist.", "")
    errInvalidGroup     = newAPIError(404, "invalid_group", "Invalid Group", "The group you requested does not exist.", "")
)

All of my other funcs in my app return their err but when it get backs to my handler I need to handle how I return it to the client. As far as I’m aware I can’t return to middleware so I’ve added my own Recovery middleware which handles APIError that I panic from my handlers, i.e.

func Recovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                switch err.(type) {
                    case *APIError:
                        apiError := err.(*APIError)
                        apiErrors := &APIErrors{
                            Errors: []*APIError{apiError},
                        }
                        c.JSON(apiError.Status, apiErrors)
                    case *APIErrors:
                        apiErrors := err.(*APIErrors)
                        c.JSON(apiErrors.Status(), apiErrors)
                }
                panic(err)
            }
        }()

        c.Next()
    }
}

Then in my handler I can do something like this:

    // Grab the set from storage
    set, err := storage.GetSet(set_uuid)

    if (err == database.RecordNotFound) {
        panic(errInvalidSet)
    } else {
        panic(errDatabase)
    }

which, in my opinion, keeps things really tidy and reduces repetition. I’ve seen that people recommend you try to use panic as little as possible, but I’m not sure if it works well in this situation? I wonder what people think of this, whether it’s a good way to go about it and, if my method isn’t great, whether there’s any recommended alternatives?

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 31 (12 by maintainers)

Most upvoted comments

@nguyenthenguyen @nazwa @elliotlings @techjanitor ok! I just finished a new API for errors.

c.Abort() // aborts the handlers chain (no error reported)
c.AbortWithStatus(code) // like Abort() but also writes the headers with the specified status code (no error reported)
c.Error(error) // it appends an error in c.Errors
c.AbortWithError(code, error) // (old c.Fail(): c.AbortWithStatus(code)  + c.Error(error)

but there are much more improvements under the hoods! c.Error(error) as been simplified, now it just takes a error, but it is much more powerful:

c.Error(err) // simple error
c.Error(err).Type(gin.ErrorTypePublic)
c.Error(err).Type(gin.ErrorTypePrivate).Meta("more data")

by default c.Error() will store the error as gin.ErrorTypePrivate

Or you guys could use the Meta as the public error.

c.Error(err).Meta(gin.H{
   "status": "this is bad",
   "error": "something really bad happened",
})

// [...]

func handleErrors(c gin.Context) {
    c.Next() // execute all the handlers

    // at this point, all the handlers finished. Let's read the errors!
    // in this example we only will use the **last error typed as public**
    // but you could iterate over all them since c.Errors is a slice!
    errorToPrint := c.Errors.ByType(gin.ErrorTypePublic).Last()
    if errorToPrint != nil && errorToPrint.Meta != nil {
        c.JSON(500, errorToPrint.Meta)
    }
}

I have something kind of similar. You can handle errors in your own middleware though, after Next() happens. I wouldn’t personally use panic like that.

error types to feed into c.JSON: https://github.com/techjanitor/pram-get/blob/master/errors/errors.go

my typical error handlers in a controller: https://github.com/techjanitor/pram-get/blob/master/controllers/index.go#L25-L36 i pass any errors to some middleware, which you can see here: https://github.com/techjanitor/pram-get/blob/master/middleware/cache.go#L35-L42

Then how to handle the error? easy. you can create a middleware! (Gin will provide a default one)

func main() {
    router := gin.Default()
    router.Use(handleErrors)
    // [...]
    router.Run(":8080")
}

func handleErrors(c gin.Context) {
    c.Next() // execute all the handlers

    // at this point, all the handlers finished. Let's read the errors!
    // in this example we only will use the **last error typed as public**
    // but you could iterate over all them since c.Errors is a slice!
    errorToPrint := c.Errors.ByType(gin.ErrorTypePublic).Last()
    if errorToPrint != nil {
        c.JSON(500, gin.H {
            "status": 500,
            "message": errorToPrint.Error(),
        })
    }
}

I use a similar, but I suppose more basic approach.

For all user-space errors I simply tell the user what’s wrong and forget about it.

    if !c.Bind(&details) {
        c.JSON(http.StatusBadRequest, gin.H{"Error": FormDataError.Error()})
        return
    }

If there is an unexpected situation or server error, I also add it to context:

    if err != nil {
        c.Error(err, err.Error())
        c.JSON(http.StatusInternalServerError, gin.H{"Error": BraintreeError.Error()})
        return
    }

Which is then picked up by my error handling middleware:

func Rollbar() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        for _, e := range c.Errors {
            rollbar.RequestError(rollbar.ERR, c.Request, errors.New(e.Err))
        }

    }
}

It does need a bit of repetition but for small projects it’s more than enough 😃

@elliotlings in my opinion, Gin provides a even better solution!

basically c.Error() helps developers to collect errors! it does not abort the request. it does not print anything either, it just collect errors! then you can do what ever you want.

then, we have c.Fail() just like c.Error(error) but c.Fail(code, error) stops the request. c.Error() can be called many times in the same request. c.Fail() should be called just once.

I handle errors like this:

func main() {
    router := gin.Default()
    router.Use(errorHandler)
    router.GET("/", func(c *gin.Context) {
        c.Fail(500, errors.New("something failed!"))
    })

    router.Run(":8080")
}

func errorHandler(c *gin.Context) {
    c.Next()

    if len(c.Errors) {
        c.JSON(-1, c.Errors) // -1 == not override the current error code
    }
}

@elliotlings

    if (err != nil) {
        c.Error(err, err.Error())
        c.JSON(ErrorMessage(errDatabase))
        return
    }

looks good to me.

c.Error() is a built helper, it is really useful when several things can fail. Remember it is just an array of errors. If you do not need it do not use it.

In my case, several things can fail, so we use c.Error(), then we have a errorMiddleware. The error middleware takes all the private errors and submit them to sentry + a log.

If there are public errors, we print them. if we only have private errors, we print ref codes:

    if !isPrivate || r.DebugMode {
        return errors.New(report.Message)
    } else {
        // If the debug mode is disabled and the report is private
        // we return error message instead the original one.
        // this is very important since there are internal errors that should not be public because of *security reasons*.
        return errors.New("internal error. Ref " + report.Id)
    }

In my case, the error logic is completely separated:

func User(c *gin.Context) {
    var req models.UserJSON
    if c.Bind(&req) {
        result, err := MODELS.C(c).UsersByIds(req.Users)
        if err == nil {
            c.JSON(200, gin.H{"users": result})
        }
    }
}

And some response like that?

{
"errors":[{
    "id": "internal_server_error",
    "status": 500,
    "title": "Internal Server Error",
    "detail": "Something went wrong."
}]
}

or

{
    "id": "internal_server_error",
    "status": 500,
    "title": "Internal Server Error",
    "detail": "Something went wrong."
}

Thanks for you reply. The way you handle your errors looks better than panicing, also I found, that if I panic I loose the reference to the Error which isn’t helpful.

I have created an ErrorMessage like yourself:

func ErrorMessage(err error, error interface{}) (int, *APIErrors) {
    var apiErrors *APIErrors

    // This the best way to log?
    trace := make([]byte, 1024)
    runtime.Stack(trace, true)
    log.Printf("ERROR: %s\n%s", err, trace)

    switch error.(type) {
        case *APIError:
            apiError := error.(*APIError)
            apiErrors = &APIErrors{
                Errors: []*APIError{apiError},
            }
        case *APIErrors:
            apiErrors = error.(*APIErrors)
        default:
            apiErrors = &APIErrors{
                Errors: []*APIError{errUnknown},
            }
    }
    return apiErrors.Status(), apiErrors
}

and then call it like such:


    // Grab the set from storage
    set, err := storage.GetSet(set_uuid)

    if (err == database.RecordNotFound) {
        c.JSON(ErrorMessage(err, errInvalidSet))
        return
    }
    if (err != nil) {
        c.JSON(ErrorMessage(err, errDatabase))
        return
    }

This seems much better. Do you think this looks good? Also, what do you think about the way I am logging the error? I want to keep the logging outside of the handler for these kind of errors (to reduce repetition), so I thought logging it when I create ErrorMessage may be the best place, also have the stack trace is helpful for debugging.