Professional Documents
Culture Documents
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
https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 1/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium
package mysql
if len(userID) != 6 {
return nil, errors.New("Invalid user ID.")
}
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"]
https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 2/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium
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")
)
package mysql
https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 3/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium
package main
// The handler
func getUser(c web.C, w http.ResponseWriter, r
*http.Request) {
userID := c.URLParams["user_id"]
// 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
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
if len(userID) != 6 {
return nil, errors.Wrapf(user.ErrInvalidUserID, "%s
is not 6-long string", userID)
}
https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 5/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium
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
}
...
}
Are there any drawbacks?
I would not say drawbacks, but you will certainly feel like
becomes
https://medium.com/devthoughts/domain-decoupled-errors-in-go-ed583a4f1dd7 6/9
3.6.2018 Domain (decoupled) errors in Go – devthoughts – Medium
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:
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