Effective Error Handling in Go
Tags: go
Published: 2024-05-29 06:57:16

---
title: Effective Error Handling in Go
published: true
date: 2024-05-28 00:00:00 UTC
tags: Go
canonical_url: https://hiro.one/blog/2024-05-28/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/31220y2iq2wof98btg6r.jpg
---

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:

```go
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:

```go
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 the server receives invalid user input, otherwise, HTTP 500.

So, how to do that??

One way to achieve it is to check error message:

```go
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:

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

```

Now, let's update our `UserService.SignUp()`:

```go
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:

```go
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 ✌️