When error messages contain “useful information” e.g. application-specific IDs or other variables then the error messages lose their “grepability”. In other words, you cannot quickly search a codebase for an error without removing anything you believe could be variable.

You might not think this is a big issue, and I might even agree with you there, but you might as well make it a practice to make error messages boring – I.e. no variables in strings.

To be clear I’m not talking about fmt.Errorf("could not foo the bar: %w", err) which uses fmt.Errorf to wrap an error using the %w format directive. I’m a proponent of wrapping errors. What I think could be problematic is something like this:

if err != nil {
	return fmt.Errorf("user %s cannot access account %s: %w", userID, accountID, err)
}

Where userID and accountID would likely differ between each time the error is generated. I argue this makes errors difficult to search for in a code base and it could even mess with your error reporting tools like Sentry. Error reporting tools often group “Events” of the same “Issue” (Sentry) / “Error” (Bugsnag) based on the attached stacktrace. Now, Go in particular doesn’t provide stacktraces in errors by default, so error reporting tools fall back on the error message for grouping events. If the error message is different because we include request- or goroutine specific data then events aren’t grouped together. A potential consequence of this is spammy notifications which in turn could lead to ignoring notifications and alerting from these tools altogether. I’m sadly speaking from experience here…

The solution I’m proposing is to follow the same pattern that structured logging libraries like Zap or Logrus follow:

Include IDs etc. as part of metadata (“fields”) instead of in the message itself.

This does mean we need an error type that supports this metadata functionality. It’s not too hard to create yourself, and if you’re curious to see what this would look like, take a look at github.com/kinbiko/rogerr.

The pattern I’m proposing in this package is as follows:

  • Attach metadata (e.g. user ID) to a context.Context variable with ctx = rogerr.WithMetadatum(ctx, "user_id", userID)
  • When you need to create an error, use err = rogerr.Wrap(ctx, err, "unable to foo the bar") to attach the metadata to an error.
  • When you log/report/otherwise handle the error use md := rogerr.Metadata(err) to get a map[string]interface{} of all metadata fields associated with the error.

Full example found here.

Once you have this metadata, you can then attach as fields to a structured log message or as tags in a report to Sentry, etc. You could even execute custom error handling logic based off the values in this map should you wish, all while maintaining grepability of your error messages.