Fun (or Not) With Golang Errors
6 error-related problems I’m trying to solve
I love Go and have been writing code with it for nearly a decade, but dealing with errors has always been a struggle. My previous company was one of the first companies in the world to use Golang in production and I’ve battled with how best to handle errors since day one. Somehow things just never felt right, so lately I started really experimenting with them to make using them feel OK.
These are the problems I wanted to solve:
- How do I get a stack trace where the error occurred?
- When and where should I write log output for the errors?
- How do I differentiate between an error I want to show a user and what I want to show up in the logs?
- How do I add context so I can use structured logging when I want to log the error?
- How can I reduce the amount of code dedicated to error handling and logging?
- How do I follow common Go best practices such as the ones suggested by Dave Cheney and accomplish the above?
I’ve tried everything at least once, just to see how it feels.
For instance, logging at the deepest point so I can get a stack trace in my logs and the proper context, and then returning the error. Logging libraries like Uber’s Zap will print the stack trace and structured content:
This always felt a bit ugly to me, as it requires an extra line for logging every time you get an error. This also means passing the logger
around by either explicitly adding it as a parameter to every function or using the context to pass it around (the cleaner way). Some would argue adding it to the context isn’t a good idea either.
And you inevitably log the same error more than once if you aren’t careful and check that it wasn’t already logged deeper down. So you end up with things like this:
You can see where this is going, and it’s not a happy place!
When you’re working on an API (which is probably most of us), things can get even worse with code like this:
Now it’s getting really, really ugly. And all this extra no-good code is all over the place on every error check (and in Go, you have error checks everywhere).
So what’s a poor Go developer to do?
My Latest Go Error Experiments
I think I’ve finally figured out a fairly elegant way to deal with everything above while keeping the error-handling code to a minimum. A revelation in my own mind.
I have a few GitHub repos that I use for experimentation, and I’ll be referencing one in particular in this section called gotils.
Step 1: Add context to the context
Instead of adding context to your logger
(structured fields), add them to the Go context instead:
ctx = gotils.With(ctx, "foo", "bar")
The nice thing about this is that you can use these contextual fields anywhere — not just for your logging library. It’s just a basic map of entries. And adding context to the context seems like a proper fit.
Step 2: Add context and a stack trace to errors
Instead of trying to log deep down so we can get a proper stack trace and context, let’s add the stack and the context to the errors we return:
if err != nil {
return gotils.C(ctx).Errorf("error on x: %v", err)
}
The gotils.Errorf
function wraps your error and adds the current stack along with the context field map and returns a new error with that information.
Step 3: Log and return the error response to the user at the entry point
If this is an API and we got an error, then we’d only deal with the error at the entry point (i.e. the HTTP handler):
So now we’ve removed all of the ugly error-handling and logging code from the innards of the application, and we only do it once at the edge.
Reduce logging and error handling even more
To make this type of handling even better, try using an ErrorHandler
that lets your HTTP handlers return an error. This means we don’t need to deal with errors in every handler — just one place that wraps all the other handlers. This looks like the following:
Then you have one place in your entire application that deals with logging and returning the errors.
You can see a full example of the ErrorHandler
that deals with UserErrors
and whatnot.
User-specific errors to hide internal errors from users and return informative messages
Oftentimes, you don’t want your users to see an error that occurred from within your program (e.g. a database error). You probably don’t want to send a sql.ErrTxDone
back to your user, but you probably want to log it or deal with it somehow.
One way to handle it is to just return generic errors, like a 500 Internal Server Error if an error makes its way out, but that’s very rigid and you can’t return a detailed response about what exactly went wrong to the user. For instance, if it was just invalid input, you’d want to return a message like “field X is invalid.”
So we need a way to differentiate error messages for internal use and error messages for users. For this, I made a UserError
interface:
type UserError interface {
error
UserError() string
}
It’s used like this:
return gotils.UserErrorf(err, "field %v is invalid", fieldname)
Then when it’s time to return your response:
There’s also a gotils.HTTPError
if you want to return specific HTTP status codes.
How to use these with your logging library
Your logging library won’t know how to extract the stack or the fields automatically, but luckily, this is pretty easy. Even better, you might only have one line in your entire app that actually uses your logging library!
If you try the above with your own code, just be sure to have a couple of methods to get the context fields and stack from the error. gotils
has Stacked
and Fielded
interfaces that provide these methods that you can copy or just use directly if you’d like:
Call those to get the right info and pass it along to your logging library.
An example of this in the gcputils repo that will log this all in the proper format for Google Cloud logging with a simple call to:
gcputils.Printf("%v", err)
Conclusion
These things have allowed me to clean up a lot of messy code that always bugged me, but I was too busy (or lazy) to figure out a better way. Until recently, that is, when I just had enough and dedicated time to experiment with logging and error handling.
Try some of the concepts discussed here and see what you think. I’d love to hear your feedback or learn how you are dealing with these things.
“To err is human; to forgive, divine.” — Alexander Pope