When building any software, specifically Microservices, there’s something we should always be aware of, something that is going to happen no matter what: errors.
Failures are always something we have to consider when building new software products, it’s part of the territory and there’s no way around it, specially when building distributed systems.
The problem is not failing but rather the lack of planning regarding monitoring and reacting to those failures.
Introduction to errors
Errors in Go are simple in a sense any type implementing the error interface is considered an error, the idea with errors is to detect them, do something with them and if needed bubble them up so the callers can also do something with them:
if err := function(); err != nil {
// something happens
return err
}
In Go 1.13 a few extra methods were added to the errors
package to handle identifying and working errors in a better way, specifically:
Instead of comparing a sentinel error using the ==
operator we can use something like:
if err == io.ErrUnexpectedEOF // Before
if errors.Is(err, io.ErrUnexpectedEOF) // After
Instead of explicitly do the type assertion we can use this function:
if e, ok := err.(*os.PathError); ok // Before
var e *os.PathError // After
if errors.As(err, &e)
- New
fmt
verb%w
and errors.Unwrap, with the idea of decorating errors with more details but still keeping the original error intact. For example:
fmt.Errorf("something failed: %w", err)
This errors.Unwrap
function is going to make more sense when looking at the code implemented below.
Implementing a custom error type with state
The code used for this post is available on Github.
Our code implements an error type called internal.Error
, this new type includes state, the idea of this state is to define an Error Code that we can use to properly render different responses on our HTTP layer. Those different responses are going to be determined by the code that is included in the error.
It looks like this:
// Error represents an error that could be wrapping another error, it includes a code for determining
// what triggered the error.
type Error struct {
orig error
msg string
code ErrorCode
}
And the supported error codes:
const (
ErrorCodeUnknown ErrorCode = iota
ErrorCodeNotFound
ErrorCodeInvalidArgument
)
With those types we can define a few extra functions to help us wrap the original errors, for example our PostgreSQL repository, uses WrapErrorf
to wrap the error and add extra details regarding what happend:
return internal.Task{}, internal.WrapErrorf(err, internal.ErrorCodeUnknown, "insert task")
Then if this error happens, the HTTP layer can react to it and render a corresponding response with the right status code:
func renderErrorResponse(w http.ResponseWriter, msg string, err error) {
resp := ErrorResponse{Error: msg}
status := http.StatusInternalServerError
var ierr *internal.Error
if !errors.As(err, &ierr) {
resp.Error = "internal error"
} else {
switch ierr.Code() {
case internal.ErrorCodeNotFound:
status = http.StatusNotFound
case internal.ErrorCodeInvalidArgument:
status = http.StatusBadRequest
}
}
renderResponse(w, resp, status)
}
Conclusion
The idea of defining your own errors is to consolidate different ways to handle them, adding state to them allows us to react differently; in our case it would be about rendering different responses depending on the code; but maybe in your use case it could main triggering different alerts or sending messages to different services.