Error Handling Using Go Type Switch

Recently while working on an issue to increase unit test coverage in the core services of the EdgeXFoundry project, I had the opportunity to explore the usefulness of type switches in Go. In order to increase the unit testability of the codebase, I needed to decouple business logic from infrastructure logic – such as the MUX router. I know that Go provides an httptest package in order to simulate the request/response functionality for unit testing purposes, but I’ve never been comfortable with the coupling I believe this allows. I think the business logic should be independently testable without any concern for the MUX router, which should itself be tested via integration/blackbox tests perhaps defined in a tool like Postman.

A challenge came in the form of what to do with errors. At the start, my controller imported the database package directly because the business logic obviously needs to talk to the database.

import "github.com/edgexfoundry/edgex-go/internal/pkg/db"

This meant I had access to errors defined in the following manner.

package db

var (
   ErrNotFound            = errors.New("Item not found")
   ErrUnsupportedDatabase = errors.New("Unsuppored database type")
   ErrInvalidObjectId     = errors.New("Invalid object ID")
   ErrNotUnique           = errors.New("Resource already exists")
)

My plan required that I firmly separate controller from database by removing the import above, which meant I needed to map the errors from the db package into new types that would be returned from the business logic. I also wanted to allow for custom error messaging, and facilitate the return of a different HTTP response code from the controller based on the error that had occurred.

I’ll take the simple use case of a request to retrieve an object by its ID where the object is not found. The database provider will return the above db.ErrNotFound to the business logic. Now, in order to return to the router an indication of what went wrong, I created the following type.

package errors
type ErrEventNotFound struct {
   id string
}

func (e ErrEventNotFound) Error() string {
   return fmt.Sprintf("no event found for id %s", e.id)
}

func NewErrEventNotFound(id string) error {
   return &ErrEventNotFound{id: id}
}

This is just one of several error types that I defined to trap various problematic conditions. In my business logic, the mapping of the error looks like this.

func getEventById(id string) (models.Event, error) {
   e, err := dbClient.EventById(id)
   if err != nil {
      if err == db.ErrNotFound {
         err = errors.NewErrEventNotFound(id)
      }
      return models.Event{}, err
   }
   return e, nil
}

The above function is called from the router so, as you can see, there’s no handling of any kind of external concern like HTTP request/response. Notice also that the function to create the error takes an “id” parameter. The returned error instance should be immutable and should conform to the error interface from the Go standard library.

In the router, I then check the type of the error returned from the business logic and adjust the HTTP status code of the response accordingly using the very handy type switch.

func getEventByIdHandler(w http.ResponseWriter, r *http.Request) {
   if r.Body != nil {
      defer r.Body.Close()
   }

   // URL parameters
   vars := mux.Vars(r)
   id := vars["id"]

   // Get the event
   e, err := getEventById(id)
   if err != nil {
      switch x := err.(type) {
      case *errors.ErrEventNotFound:
         http.Error(w, x.Error(), http.StatusNotFound)
      default:
         http.Error(w, x.Error(), http.StatusInternalServerError)
      }

      LoggingClient.Error(err.Error())
      return
   }

   encode(e, w)
}

You might notice that when instantiating the custom ErrEventNotFound type that the above code returns a pointer. When I originally wrote this solution, the type switch in the function above was written as follows and it did not work. Can you spot why?

switch x := err.(type) {
      case errors.ErrEventNotFound:
         http.Error(w, x.Error(), http.StatusNotFound)
      default:
         http.Error(w, x.Error(), http.StatusInternalServerError)
      }

With the code in this state, the default case always executed. Having come from a heavy .NET background, I’ve never had to be pointer conscious. I expected that I could just interrogate the type of an object without taking whether or not it was actually a reference into account. But the fact that the * is missing from the

case errors.ErrEventNotFound:

makes a world of difference in Go. Don’t take this as a gripe against the Go language, it just is what it is. But it was a very subtle bug that I had a hard time finding since I’ve never had to look for it.

I’m actually rethinking the use of pointers at all in the above logic. The recommendation for when something should be a value versus reference type includes if the object is small, then the overhead of copying it by value when returned from function B to function A is negligible. My error types are very short-lived immutable types with one private field and one public method, so I think they fall into this category of value objects. The official Go code review guidelines would support my changing this implementation, so I plan to do that shortly.

In short, I hope this article is helpful on the following 3 points:

  • Custom error handling to facilitate decoupling of concerns in your application
  • How switch based type assertions work in Go
  • Anecdotal evidence for when and when not to use pointers

 

Leave a Reply