I will not get into the philosophies of error handling or the details of how to implement an error handler. But the relationship between HTTP handlers and error handlers is an important one to consider.
No one way is superior to another, but each has advantages and disadvantages that should be considered.
1. http.Error()
Our friendly neighborhood standard library function, http.Error(), is easy to use and is instantly available to any Go web application.
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := riskyFunc(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
The code is clean, easy to read, and idiomadic. You call the error function and return on the next line. However, the client will see the error message. This is a security doozy if the error contains any details about the system. It's a bit jarring for a user to see technical messages, especially when they are unexpected. Oh, and if your app services users with a browser, they'll see something like this,
This isn't all bad. It's easy to replace the error message with a JSON response if your web app is being consumed by API clients. If you're clever, you might think, "Ha! I'll just write an HTML body as the error message and the browser will render a nice error page."
Hate to tell you this, but of the three whole lines that comprise http.Error(), the first one is devoted to setting the Content-Type to plaintext:
Also, pro tip: your clients are not error logs. http.Error() alone does not provide a way to log the error (not without some clever wrapping of the ResponseWriter), so unless your users tell you about the error - yeah, right - you won't know it happened. Yikes.
2. handleError()
One upgrade from the std lib function is to write your own error handling function. It could work very similarly:
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := riskyFunc(r)
if err != nil {
handleError(w, r, err)
return
}
}
func handleError(w http.ResponseWriter, r *http.Request, err error) {
log.Println(err)
fmt.Fprintln(w, "<html>...</html>")
}
This allows you to log the error, write an HTML response if desired, and it's no harder to use than http.Error() - plus you can pass in the request to know what provoked the error. You might also pass in a status code, which I neglected here (the example writes a 200 OK status - don't do this).
Problem: Where is your handleError() defined? If it's with your HTTP handlers, then your application still doesn't have control over error handling. If your HTTP handlers are imported by more than one package or application, this is bad. It could also be argued that error handling doesn't belong scattered among your HTTP handlers.
This is mostly easy to fix. In the type (usually a struct) that has the ServeHTTP() method, just add a field which is an error function:
type MyHandler struct {
...
ErrorFunc func(http.ResponseWriter, *http.Request, error)
}
Now the application can set the error function, as long as it matches the signature given, so make sure to choose a good one. Also, you're not home free: just make sure the function isn't nil before calling it.
This might seem to be perfect, but a more subtle condition exists, especially in a middleware environment which is common in Go applications. Middleware chains HTTP handlers together, and each handler takes care of a particular task related to the request.
Your middleware stack grows as your application grows, but no matter how long your middleware chain gets, you've got only one shot to call WriteHeader and to write the response body. (Okay, you can call Write multiple times, but that's usually a bug.)
So malfunctioning middleware becomes a problem: a middleware layer may not write to the response when it should, or it does write to the response when it shouldn't - even in error handling. Neither is acceptable because, in the former case, the client gets a blank response, and in the latter, who knows what will happen - I've had browsers download corrupted gz files every time the user clicked a link because of malfunctioning middleware.
What we need is a way to guarantee that a response is only written once no matter what happens, while keeping error handling close to our application and away from the handlers.
3. return int, error
What I am suggesting here is to add return values to HTTP handlers.
Don't freak out - this wasn't my idea. It actually comes from the Go blog, and I've adapted it in this talk. The first value is the status code, and the second is the error, if any.
Here's how it looks:
func ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
err := riskyFunc(r)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
Notice that your HTTP handler has no notion of error handling - it doesn't even call an error function! This is a natural concept for middleware and works really well in a chain of handlers.
A simple contract guarantees robust request handling: if a handler returns an error status (>= 400), then the response has not yet been written and the client must still be shown an error message. If the error value is not nil, then something went wrong on the server and it should be logged/reported.
In other words, the first return value is for the client's benefit, and the second return value is for the server. They are completely independent; an error status doesn't always mean the error will be non-nil. (For example, 404 Not Found is not usually a server error.)
So where is the error actually handled, and how do we make this compatible with net/http?
Consider this at/near the application layer:
type appHandler func(http.ResponseWriter, *http.Request) (int, error)
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
status, err := fn(w, r)
if status >= 400 {
http.Error(w, http.StatusText(status), status)
}
if err != nil {
log.Println(err)
}
}
We simply define a type that implements net/http.Handler which invokes the non-net/http middleware chain. The base handler, which belongs to the application, can handle the errors. Each middleware gets a guarantee that the response is properly handled and error handling is no longer scattered around your HTTP handlers, nor are they coupled to your package/application.
Example
You can see this pattern in action with Caddy, which is a batteries-included HTTP/2 web server. Caddy actually offloads the error handling to a separate middleware layer rather than doing it in the application's ServeHTTP method - but either way is fine, it just depends on your architecture. (Check out that link for the example!)
Notice how, at that link, the error handler returns a status < 400 after it writes the error page to the client, signaling to any other middleware layers that a response has been written.
Check out the other middleware in that repository if you want to get a better idea how it works.
Conclusion
Again, none of these are inherently right or wrong - do what suits you best. These are not the only ways to handle errors in HTTP handlers, either, but I merely wanted to present a few ways that you might find helpful to adopt in your own applications.