Effective Error Handling in Go
One part of why I like Go is how it forces me to handle errors. It has panic mechanizm but most of the time a callee returns an error, and a caller handles it.
However in practical cases, returning an error isn’t enough to handle for caller. For example, let’s imagine we have the following signup http handler:
func HandleSignUp(us UserService) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ...decode r.body, etc.
userLoggedIn, err := us.SignUp(userSigningUp)
if err != nil {
encodeJson(w, http.StatusBadRequest, Response{ Message: "bad request" })
return
}
// ... respond HTTP 201
})
}
And here is SignUp method in UserService:
func (s *UserService) SignUp(user *domain.UserSigningUp) (*domain.UserLoggedIn, error) {
if u, _ := s.repository.Get(user.Email); u != nil {
return nil, errors.New("user already exists")
}
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5)
if err != nil {
return nil, err
}
udb := user.ToUserInDB(hash, time.Now())
if err = s.repository.Add(udb); err != nil {
return nil, err
}
return udb.ToLoggedIn(), nil
}
Here is a problem - UserService.SignUp()
method could return two kinds of error:
- invalid user input error
- or, database error
And we want to respond HTTP 400 when we see invalid user error, otherwise, HTTP 500.
So, how to do that??
One way to achieve it is to check error message:
userLoggedIn, err := us.SignUp(userSigningUp)
if err != nil {
if err.Error() == "user already exists" {
encodeJson(w, http.StatusBadRequest, Response{ Message: "bad request" })
return
}
encodeJson(w, http.InternalServerError, Response{ Message: "internal server error" })
return
}
This case is somehow straight forward, but obviously not a good solution. For example, the above approach is prone to code update. If we make a change to the error message in UserService.SingUp()
we might need to change our HTTP handler too.
One better approach is to implement custom error.
As you might know everything that has Error()
method will be regarded as of type error
.
Also, you can add Is()
method to your custom error struct so that you can validate it using errors.Is()
.
Here is an example of how to implement it:
First let’s create our custom error structs:
package util
type AppError struct {
Status int
detail string
}
func (e AppError) Error() string {
return e.detail
}
func (e AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return t.Status == e.Status && t.detail == e.detail
}
var BadRequestError = AppError{Status: 400, detail: "bad Request"}
var InternalServerError = AppError{Status: 500, detail: "internal server request"}
Next, let’s update our UserService.SignUp()
:
func (s *UserService) SignUp(user *domain.UserSigningUp) (*domain.UserLoggedIn, error) {
if u, _ := s.repository.Get(user.Email); u != nil {
fmt.Println("invalid user input")
return nil, util.BadRequestError
}
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5)
if err != nil {
fmt.Println("error generating new hashed password: ", err)
return nil, util.BadRequestError
}
udb := user.ToUserInDB(hash, time.Now())
if err = s.repository.Add(udb); err != nil {
fmt.Println("error storing user data: ", err)
return nil, util.InternalServerError
}
return udb.ToLoggedIn(), nil
}
Here, you can see, instead of returning err
, we return util.BadRequestError
or util.InternalServerError
depending on how we want to handle it.
Then, let’s update our handler function too:
func HandleSignUp(us UserService) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ...decode r.body, etc.
userLoggedIn, err := us.SignUp(userSigningUp)
if err != nil {
if errors.Is(err, util.BadRequestError) {
encodeJson(w, http.StatusBadRequest, Response{
Message: "bad request",
})
} else {
encodeJson(w, http.StatusInternalServerError, Response{
Message: "internal server error",
})
}
return
}
// ... respond HTTP 201
})
}
Thanks for reading ✌️