rss resume / curriculum vitae linkedin linkedin gitlab github twitter mastodon instagram
Microservices in Go: Implementing and Dealing with errors
May 11, 2021

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.


Back to posts