Application Specific Error Handling | Crit Russell

February 26, 2018

Application Specific Error Handling

Go’s standard error is extremely simple to use which is a feature I like. It lets you add context by implementing its interface only when your application needs to. This can make cross boundary design simple and flexible.

An API/RPC web service might be a good illustration.

Some assumptions about this example are that your clients are mostly HTTP and care about the HTTP Status Code returned from the server. Also, that you are trying to separate your business logic from your web logic.

net/http usage:

func getUser(w http.ResponseWriter, userID string) {
	user, err := users.Get(userID) // business logic; all handling of validation, etc

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		// ugh, what about not found? unauthorized?
		// userID is a string so it might need to be decoded.
		// users.Get may be returning an error indicating
		// a Bad Request if the userID can't be decoded. All useful contextual
		// info for the client (possibly).
		// All errors *can't* be internal server error!
		return
	}

	fmt.Fprint(w, toJSON(user)) // toJSON not shown.
}

echo usage:

func getUser(context echo.Context) error {
	user, err := users.Get(context.Param("userID"))

	if err != nil {
		return c.JSON(http.StatusInternalServerError, err)
		// again, not found? unauthorized? etc...
	}

	return c.JSON(http.StatusOK, user)
}

There are a lot of ways to design the return from users.Get to cover for these different error possibilities. Returning more than just two values or perhaps composing an enum. Since errors tend to be rare (in my experience relative to successful requests), I like to make the err return value have extra information that can be inspected if it is populated.

A more nuanced error handling process might look like this. Staying with echo:

func getUser(context echo.Context) error {
	user, err := users.Get(context.Param("userID"))

	if err != nil {
		return c.JSON(errors.Code(err), errors.ErrorPackage(err))
		// now `users.Get` can tell us what variety of error it encountered.
		// ErrorPackage gets us an "Error Contract" that the client can rely on.
	}

	return c.JSON(http.StatusOK, user)
}

What could users.Get look like?

package users

// imports, types, other funcs, etc ...

func Get(id string) (user User, err error) {
	user.ID, err = tokens.ToID(id) // decode id token

	if err != nil {
		// ok, this is a client error. cool
		return user, errors.Error(http.StatusBadRequest, err)
		// or we could log `err` and use the following to keep
		// info from the client:
		// return user, errors.ErrorString(http.StatusNotFound, "user not found")
	}

	err = db.Find(&user) // look up in repo
	// assuming a project specific implementation of db.Find
	// we may be getting a errors.Err struct so we can just return it.
	// Even if we are not, our implementation should handle being
	// passed native errors.

	return user, err
}

And finally, a possible implementation:

package errors

import (
	"fmt"
	"net/http"
)

// Err is the base error structure.
type Err struct {
	Code    int
	Message string
}

// Error returns error code and message in a formatted string. Satisfies
// the error interface.
func (e Err) Error() string {
	return fmt.Sprintf("[%d] - %s", e.Code, e.Message)
}

// Code attempts to cast the error to Err and determines the code. Nil is 
// 200; Non-Err is 500.
func Code(err error) int {
	if err == nil {
		return http.StatusOK
	}

	e, ok := err.(Err)

	if !ok {
		return http.StatusInternalServerError
	}

	return e.Code
}

// Message attempts to cast the error to Err and determines the message. Nil is an empty
// string; Non-Err is the error interface output.
func Message(err error) string {
	if err == nil {
		return ""
	}

	e, ok := err.(Err)

	if !ok {
		return err.Error()
	}

	return e.Message
}

// Error is a convenience function that creates a new Err with code and 
// the Message of the error.
func Error(code int, err error) error {
	return Err{code, Message(err)}
}

// ErrorString is a convenience function that creates a new Err with code and a message
func ErrorString(code int, msg string) error {
	return Err{code, msg}
}

// ErrorPackage is a convenience function to output a standard response for
// an HTTP handler of our specific API Service.
func ErrorPackage(err error) map[string]interface{} {
	if err == nil {
		return nil
	}

	return map[string]interface{}{
		"code":    Code(err),
		"message": Message(err),
	}
}

Comments/Questions?

© Crit Russell