Every so often, go’s error handling pops up in various forums and everyone seems
to have an opinion about it. Some say they should be more like throwable
exceptions, others prefer sum types like rust’s Result<T, E>
. While I’ve gone
with the sum type approach in typescript,
I still like the way go handles errors.
That said, figuring out how to really handle errors can take some time (with
or without sum types/exceptions). In this post, I’ll be walking through one
approach on handling errors in go’s http.Handler
.
The classical example Link to heading
The error values can be frustrating if you expect them to scale out “just like that” without repeating themselves. Usual example goes something like this:
func copyfile(src, dst) error {
fsrc, err := os.Open(src)
if err != nil {
return err
}
defer fsrc.Close()
fdst, err := os.Open(src)
if err != nil {
return err
}
defer fdst.Close()
err := io.Copy(fdst, fsrc)
return err
}
Thats not so bad, and I’m quite sure that most of you have seen example like
that before. But let’s take a look at similar situation that might occur in a
http.Hanlder
:
func handleThing(w http.ResponseWriter, r *http.Request) {
// Our path is something like /thing/3
id, err := idFromPath(r.URL.Path)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusNotFound)
return
}
thing, err := store.GetThingByID(id)
if err != nil {
// The error might be sql.NoRows, or it might be something else.
if store.IsNotFoundErr(err) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
log.Printf("Failed to get a thing: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
acc := AccountFromRequest(r)
if acc == nil {
// No account attached to the request's session -> permission denied.
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
has, err := thing.HasPermissionToView(acc)
if err != nil {
// For some reason, we failed to check permissions. Better log it.
log.Printf("Failed to check permissions: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if !has {
// Permission denied.
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
// All good, send data to the client.
respond(w, r, decodeThing(thing))
}
Theres some functions that you’ll have to imagine is defined somewhere, but the functionality is this:
- Extract ID from URL
- Use that ID to get the thing from database
- Check if the client has permission to view the thing
- Give the thing to the client
This functionality probably repeats itself for other resource types, so it gets repetitive quite fast. Imagine doing the same for resources like foo, bar, account and so on! Same functionality can be written in Django like this:
def handle_thing(request):
id = id_or_bad_request(request)
thing = thing_or_404(id)
account = account_or_forbidden(request)
if not thing.has_permission(account):
raise Forbidden()
return JsonResponse(...)
Now that’s quite a lot simpler, thanks to throwable errors and how they
can be used to disrupt the code’s flow. id_or_bad_request
, thing_or_404
and
account_or_forbidden
all throw an error that someone catches somewhere higher
and does the appropriate thing, like respond with correct status code and log
any errors.
Trying to simplify it Link to heading
Keeping that python code in mind, let’s think what we could do in our go code to get it a bit more terse:
- When an error occurs, we just want to “throw” it somewhere. It usually is a client error, but not always. Perhaps someone else can figure that out?
- If a non client error occurs, it needs to be logged somewhere
- Someone else should be able figure out the
http.Error
calls
Golang’s error handling and Go talks a bit about error handling in your http handlers and gives the following example:
func viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}
// NOTE: the following is my adapted version from the example's ServeHTTP to a
// middleware/wrapper
type HandlerE = func(w http.ResponseWriter, r *http.Request) error
func WithError(h HandlerE) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
}
That already solves one of our problems, which is the http.Error
call. But
sometimes we don’t want to expose detailed errors to the client, so I would
replace the actual message with just a generic internal server error message.
Also, logging the reasons for internal server errors is important, so that you
can figure out what went wrong.
Improving the WithError wrapper Link to heading
We want to return an error from our actual http.Handler
, but somehow
instruct the WithError
wrapper function to respond correctly to the client
when it gets an error, and on some errors log the errors. Something like this:
func WithError(h HandlerE) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
if is404err(err) {
http.Error(w, "not found", 404)
return
}
if isBadRequest(err) {
http.Error(w, "bad request", 400)
return
}
// Some other special cases...
// ...
log.Printf("Something went wrong: %v", err)
http.Error(w, "Internal server error", 500)
}
}
}
Hmm, those “other special” cases might come and go and might get quite specific
for some handlers. Also, we’d still need to write those is404err
and
isBadRequest
handlers and whatever will follow. We can do much better with an
interface:
type ErrorResponder interface {
// RespondError writes an error message to w. If it doesn't know what to
// respond, it returns false.
RespondError(w http.ResponseWriter, r *http.Request) bool
}
With this interface we can do quite powerful things. Our WithError
turns into
this:
func WithError(h HandlerE) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
if er, ok := err.(ErrorResponder); ok {
if er.RespondError(w, r) {
return
}
}
log.Printf("Something went wrong: %v", err)
http.Error(w, "Internal server error", 500)
}
}
}
Notice how our special cases just disappeared? They are now just another
implementation(s) of ErrorResponder
. This is what would our Not found
and Bad request
errors now looks like:
// BadRequest error responds with bad request status code, and optionally with
// a json body.
type BadRequestError struct {
err error
body interface{}
}
func BadRequest(err error) *BadRequestError {
return &BadRequestError{err: err}
}
func BadRequestWithBody(body interface{}) *BadRequestError {
return &BadRequestError{body: body}
}
func (e *BadRequestError) RespondError(w http.ResponseWriter, r *http.Request) bool {
if e.body == nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(e.body)
if err != nil {
log.Printf("Failed to encode a response: %v", err)
}
}
return true
}
func (e *BadRequestError) Error() string {
return e.err.Error()
}
// Maybe404Error responds with not found status code, if its supplied error
// is sql.ErrNoRows.
type Maybe404Error struct {
err error
}
func Maybe404(err error) *Maybe404Error {
return &Maybe404Error{err: err}
}
func (e *Maybe404Error) Error() string {
return fmt.Sprintf("Maybe404: %v", e.err.Error())
}
func (e *Maybe404Error) Is404() bool {
return errors.Is(e.err, sql.ErrNoRows)
}
func (e *Maybe404Error) RespondError(w http.ResponseWriter, r *http.Request) bool {
if !e.Is404() {
return false
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return true
}
You could easily write more ErrorResponder
s for permission denied errors and
much more.
Where we’re at Link to heading
With ErrorResponder
and WithError
, we can reduce our earlier handleThing
handler into this:
func handleThing(w http.ResponseWriter, r *http.Request) error {
// Our path is something like /thing/3
id, err := idFromPath(r.URL.Path)
if err != nil {
// Literally bad request. We could use BadRequestWithBody to
// respond with a fancy information for the client.
return BadRequest(err)
}
thing, err := store.GetThingByID(id)
if err != nil {
// Likely a not found issue, but something else might have gone wrong.
// Maybe404Error handles both cases.
return Maybe404(err)
}
acc := AccountFromRequest(r)
if acc == nil {
// No account attached to the request. Client needs to authenticate.
return AuthenticationRequired()
}
has, err := thing.HasPermissionToView(acc)
if err != nil {
// Something actually went wrong. Error will be logged and 500 message
// sent to the client.
return err
}
if !has {
// Client doesn't have permission to view this resource.
return PermissionDenied()
}
// All good, send data to the client.
respond(w, r, decodeThing(thing))
}
func main() {
...
mux.Handle("/thing/", WithError(handleThing))
...
}
Thats a lot better! I’ll leave it as an exercise to the reader to combine the auth and permission checking. Another exercise is to do a bit better logging than just “Something went wrong: error” in the WithError function. Perhaps log the path and requester, or use trace ids?
With all this, we can now:
- “throw” errors somewhere
- Someone else is figuring out the
http.Error
calls - Non client errors are logged
Final thoughts Link to heading
Sometimes I’m amazed just by how simple (yet powerful) go’s error type is. Other times I’m banging my head against the wall because I can’t figure out how to use that simplicity. The solution presented in this blog post is really simple, but it took me (for me) an embarrassingly long time to “figure out”.
I’ve written quite a lot of rust too, and I like to think that I would come up with a similar solution quite a bit faster in rust than in go. But that rust solution would probably be suboptimal because it would be “premature”. I’ve written a lot of that “log error, call http.Error and return” error handling and lived with the pain of that repetition long enough to see all the various use cases. With rust, I probably would have rushed into a generalization too soon, and got stuck with bad generalization.
Lastly, I’ve noticed that less experienced developers don’t necessarily have
the courage to go out and write an WithError
wrapper that would be used across
all the project’s code base. They expect that the tools they use provide such
general functionality, like django does. Or maybe they “know” go, but don’t know
go (and its philosophy)? Dunno, maybe I’m just reflecting my own experiences
here.