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)
@nguyenthenguyen @nazwa @elliotlings @techjanitor ok! I just finished a new API for errors.
but there are much more improvements under the hoods!
c.Error(error)as been simplified, now it just takes aerror, but it is much more powerful:by default c.Error() will store the error as
gin.ErrorTypePrivateOr you guys could use the Meta as the public error.
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)
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 there is an unexpected situation or server error, I also add it to context:
Which is then picked up by my error handling middleware:
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:
@elliotlings
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:
In my case, the error logic is completely separated:
And some response like that?
or
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:
and then call it like such:
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.