You are on page 1of 8

3.6.

2018 Domain (decoupled) errors in Go – devthoughts – Medium

José Carlos Chávez
Software Engineer @Typeform. Wine lover and Llama ambassador
Feb 28 · 5 min read

Domain (decoupled) errors in Go

Unrelated picture to catch your interest

While working with Go you may notice that an idiomatic pattern for
propagating errors is to return the error as the last value in the
function. For example:

package user

type User struct {


...
}

type Repository interface {


// Returns a user.User having the provided userID
GetUser(userID string) (*User, error)
}

Repository is an interface, so let’s have a look at the MySQL


implementation:

https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 1/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium

package mysql

type usersRepository struct {


db *sqlx.DB
}

const getUserQuery := "SELECT * FROM users WHERE user_id=$1"

func (ur *usersRepository) GetUser(userID string) (*User,


error) {
user := &User{}

if len(userID) != 6 {
return nil, errors.New("Invalid user ID.")
}

if err := ur.db.Get(user, getUserQuery, userID); err !=


nil {
return nil, err
}

return user, nil


}

This method works pretty well and does what it is expected, however
when using it up in the stack:

package main

// The handler
func GetUser(c web.C, w http.ResponseWriter, r
*http.Request) {
userID := c.URLParams["user_id"]

user, err := repository.getUser(userID)


if err != nil {
if err == sql.ErrNoRows {
// Returns 404
} else {
// Presumably the invalid ID error.
}
}
...
}

This approach has a couple of problems:

https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 2/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium

• The handler must now know the implementation details: in this


speci c case, handler has to know how to deal with sql errors,
for example, it should know that error sql.ErrNoRows matches
with a 404 status code which makes the usage of interfaces
pointless, coupling to speci c implementations.

• Since the handler must be aware of each error type returned by


repository.getUser , introducing a new storage will be a very
di cult task as you also need to change the handler to treat the
new implementation errors.

What about de ning errors along with the
interface?
That is a good idea, such a method describes a use case and that use
case should include its own use case errors:

package user

var (
ErrInvalidUserID = errors.New("userID should be 6 length
string")
ErrUserNotFound = errors.New("user not found")
)

type repository interface{


// Returns the ErrInvalidUserID error when a user ID is
malformed.
// Returns the ErrUserNotFound error when the user can
not be found.
// Returns the ErrPersistanceFailure error when there is
a persistance failure.
// Returns a user.User having the provided userID
GetUser(userID string) (*User, error)
}

And the MySQL implementation might look like:

package mysql

type userRepository struct {


db *sqlx.DB
}

https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 3/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium

const getUserQuery := "SELECT * FROM users WHERE user_id=$1"

func (ur *usersRepository) GetUser(userID string)


(*user.User, error) {
user := &user.User{}
if len(userID) != 6 {
return nil, user.ErrInvalidUserID
}

if err := ur.db.Get(user, getUserQuery, userID); err !=


nil {
if err == sql.NoRows {
return nil, user.ErrUserNotFound
} else {
return nil, user.ErrPersistanceFailure
}
}

return user, nil


}

Then when using it in handlers:

package main

// The handler
func getUser(c web.C, w http.ResponseWriter, r
*http.Request) {
userID := c.URLParams["user_id"]

user, err := repository.getUser(userID)


if err != nil {
if err == user.ErrUserNotFound {
// Returns 404
} else if err == user.ErrInvalidUserID{
// Returns 403
}

// Returns 500
}
...
}

That looks great, but there is a lack of context in the errors. Domain
errors lose all the context of the implementation details which makes it
impossible to debug an error. A log record of this error looks like:

https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 4/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium

2017/09/24 09:47:40.071222 [ERROR] user not found

So what is underneath this user not found error? Is it that there are no
records for this ID? Is it that the users table is locked? Is it that the
database is down? It could be any of these three reasons, but when
returning the domain error, we lose all the context of the underlying
error.

pkg/errors to the rescue.
Fortunately there is a very smart package that solves this problem.
pkg/errors provides a method that wraps an error, providing context
from bottom to top while keeping the underlying error as cause.

package mysql

type userRepository struct {


db *sqlx.DB
}

const getUserQuery := "SELECT * FROM users WHERE user_id=$1"

func (ur *usersRepository) GetUser(userID string) (*User,


error) {
user := &User{}

if len(userID) != 6 {
return nil, errors.Wrapf(user.ErrInvalidUserID, "%s
is not 6-long string", userID)
}

if err := ur.db.Get(u, getUserQuery, userID); err != nil


{
if err == sql.NoRows {
return nil, errors.Wrap(user.ErrUserNotFound,
err.Error())
} else {
return nil,
errors.Wrap(user.ErrPersistanceFailure, err.Error())
}
}

return user, nil


}

https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 5/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium

And the handler now looks like:

package main

// The handler
func getUser(c web.C, w http.ResponseWriter, r
*http.Request) {
userID := c.URLParams["user_id"]
user, err := repository.getUser(userID)

if err != nil {
if errors.Cause(err) == user.ErrUserNotFound {
// Returns 404, and drop err.Error() to the logs
} else if errors.Cause(err) == user.ErrInvalidUserID
{
// Returns 403, and drop err.Error() to the logs
}
// Returns 500
}
...
}

So this log record now looks like:

2017/09/24 09:47:40.071222 [ERROR] user could not be found.


database/sql: no rows

Are there any drawbacks?
I would not say drawbacks, but you will certainly feel like

func (r *userRepository) GetActiveUser() (*user.User, error)


{
userID := r.GetActiveUserID()
return r.GetUser(userID)
}

becomes

https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 6/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium

func (r *userRepository) GetActiveUser() (*user.User, error)


{
userID := r.GetActiveUserID()
user, err := r.GetUser(userID)
if err != nil {
return nil, errors.Wrap(ErrActiveUserNotFound,
err.Error())
}

return user, err


}

which is a few lines longer, but not too much.

Another valid concern is that since errors are variables and exported
they can be changed in other packages, that is true however it sounds
to me more as a culture problem than a problem of the approach itself.

Wrap up
We tackled some problems here:

• Errors are not describing an implementation error anymore,


but a use-case (application) error, making the error handling
implementation agnostic (decoupling, decoupling, decoupling)

• Errors carry context. A concern could be that when wrapping an


error the implementation details become a non-structured string
( err.Error() ) but that is OK, you do not need structured
information up in the stack, otherwise that will end up in
coupling.

• Having context in errors is crucial when debugging, this


approach allows us not only to keep the error context to one layer
up, but to the top of the stack, without coupling to
implementation details:

2017/09/24 09:47:40.071222 [ERROR] active user not found.


user could not be found. database/sql: no rows

https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 7/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium

Other approaches
There is a very educative post from Dave Cheney about handling errors
in go. Worth to read and choose what works better for you.

https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 8/9

You might also like