DEV Community

Mario Carrion
Mario Carrion

Posted on • Originally published at mariocarrion.com on

Building Microservices in Go: REST APIs: Implementing and Dealing with errors

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
  • 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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

And the supported error codes:

const (
    ErrorCodeUnknown ErrorCode = iota
    ErrorCodeNotFound
    ErrorCodeInvalidArgument
)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)