DEV Community

Cover image for Effective Error Handling in Go
hiro
hiro

Posted on • Originally published at hiro.one on

Effective Error Handling in Go

#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
    })
}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 the server receives invalid user input, 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
}

Enter fullscreen mode Exit fullscreen mode

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"}

Enter fullscreen mode Exit fullscreen mode

Now, 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
}

Enter fullscreen mode Exit fullscreen mode

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
    })
}

Enter fullscreen mode Exit fullscreen mode

Thanks for reading ✌️

Top comments (0)