Error handling in Go web apps shouldn't be so awkward

January 11, 2024

A quick interruption to our trek through the Go spec, to mention a new long-form article I just wrote. Jump straight to the full article


In this post I’m going to describe an error-handling pattern I’ve found to be fairly elegant when writing REST, gRPC, or other services in Go. I have three goals in writing this post:

  1. To explain the pattern I’ve implemented for a few clients, so that others developing on the same codebase will understand it.
  2. To give others a pattern they may wish to implement in their own applications.
  3. To solicit feedback. Is there a better pattern out there I haven’t seen yet? Are there tweaks I can make to this pattern to make it better?

For simplicty, the examples I’ll be discussing will be part of a REST API, using simple HTTP status codes. But the same principles can be used for gRPC services as well, or even arbitrary error codes for a CLI tool. Or even all of the above simultaneously, which I may expand on in a later post.

The problem

Before I explain the pattern I use, let me explain what it replaces, so that we might understand the problems it’s meant to solve.

Let’s look at a simple HTTP handler, using the standard library’s HandlerFunc pattern, which simply fetches a widget record from the database, and serves it to the client as JSON

func (s *Service) GetWidget(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	id, err := strconv.Atoi(r.Form.Get("widget_id"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	widget, err := s.db.GetWidget(id)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, err.Error(), http.StatusNotFound)
			return
		}
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	widgetJSON, err := json.Marshal(widget)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header.Set("Content-Type", "application/json")
	w.Write(widgetJSON)
}

While this should be a more or less realistic example, it’s intentionally over-simplified from what I typically find in production services. In particular, I’ve never seen http.Error used in a real service. Much more likely, you’ll have a custom error format you want to send back. Possibly utilizing a JSON error response, with some additional error context or internal error codes, etc. Or maybe you want to render the error as HTML. In any case, I’ll assume your app replaces the http.Error() call with something more sophisticated. Which likely means your code is even more annoying and repetitive than what I’ve shown above.

That aside, let me call out a few specific problems I see with the above code:

  • The error handling is repetitive, and non-idiomatic. Go is (in)famous for its if err != nil { return err } idiom. Yet we can’t even use that here, because the [https://pkg.go.dev/net/http#HandlerFunc] signature doesn’t return an error. Instead, for every error, we must (a) serve the error, and separately (b) return.
  • We must explicitly handle the HTTP status for every error case. If you have dozens or hundreds of handlers (and you probably do), this quickly becomes repetitive, and error-prone. There’s no DRY here. In a single handler like this, maybe it’s not a big deal. But it would be nice if we had some sort of default HTTP status code for an error—probably 500 / Internal Server Error.
  • This handler has to concern itself with database internals. In particular, it checks whether we received a sql.ErrNoRows error. The HTTP handler should be completely database agnostic, so this detail should not need to be exposed here. This is some ugly tight-coupling we can get rid of.

What if, instead…

… Read the full article at boldlygo.tech


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .