Preface
Custom error wrapping is a way to bring information or context to top level function caller. So you as the developer knows just what causes the error for a program and not receive gibberish message from the machine.
With error wrapper, You can bring interesting information like what HTTP response code should be used, where the line and location of the error happened or the information of the error itself.
If you for example build a REST API and there's an error in a transaction, you want to know what error is, so you can response to a client with proper response code and proper error message.
If you're database is out of service, you don't want to send 400 Bad Request
response code, you want 503 Service Unavailable
. You can always use 500 Internal Server Error
but that's really vague, and you as the developer will need more time to identify the error. More time identifying error means less business uptime, and you don't want less business uptime. How to fulfill this goal? We give contexts to our errors.
This article is written when Golang 1.17 is released.
The methods in this article can be deprecated or considered not optimal in the future. so be vigilant of golang updates.
Warning: This Article Assumes You Use Golang 1.13 or above
There's an overhaul for more ergonomic error handling in Golang 1.13. The errors library in Golang support more robust error checking via calling Is(err error, target error) bool
and As(err error, target error) bool
. Which you should use since this will help with potential bugs.
Creating Error Wrapper
Error Wrapper can be easily made by implementing error
interface. The error
interface has this definition:
type error interface {
Error() string
}
Off-topic Note: Because error
interface is part of standard runtime library, it's always in scope, thus it uses lowercase letters instead of Pascal Case.
So now, let's make our own custom error wrapper.
type CustomErrorWrapper struct{}
Let's leave the definition empty for now.
To implement error, you only have to add this below:
func (err CustomErrorWrapper) Error() string {
return "custom error wrapper"
}
Congratulations! Now your CustomErrorWrapper
has implemented error
interface. You can prove this by creating a function like snippet below, and check if the compiler will complain (spoiler: it will not!)
func NewErrorWrapper() error {
return CustomErrorWrapper{}
}
Obviously it is useless right now. Calling the Error()
method will only produce hard-coded "custom error wrapper"
. We need to customize the error wrapper so it can do what we intended it to be.
type CustomErrorWrapper struct{}
func (err CustomErrorWrapper) Error() string {
return "custom error wrapper"
}
func NewErrorWrapper() error {
return CustomErrorWrapper{}
}
func main() {
err := NewErrorWrapper()
fmt.Println(err.Error()) // Will only report "custom error wrapper"
}
Customizing Error Wrapper
Let's continue with REST API theme. We want to send error information from our API logic to HTTP Handler, so let's fill the struct with useful types.
type CustomErrorWrapper struct {
Message string `json:"message"` // Human readable message for clients
Code int `json:"-"` // HTTP Status code. We use `-` to skip json marshaling.
Err error `json:"-"` // The original error. Same reason as above.
}
Let's also update the Constructor
function to ensure the struct will always be filled when we use the CustomErrorWrapper
.
func NewErrorWrapper(code int, err error, message string) error {
return CustomErrorWrapper{
Message: message,
Code: code,
Err: err,
}
}
Don't forget to modify the error
implementation as well.
// Returns Message if Err is nil. You can handle custom implementation of your own.
func (err CustomErrorWrapper) Error() string {
// guard against panics
if err.Err != nil {
return err.Err.Error()
}
return err.Message
}
Ok, for now it's kind of obvious what the error wrapper is intended to be. We wrap our original error into a new kind of error, with http status code information, original error for logging. But before we continue to http handler, we have to address the following question first:
how do we do equality check for the original error?
We don't really want the CustomErrorWrapper
when we do equality check, we want to check against the original error.
This following snippet will show you what the equality check problem is.
func main() {
errA := errors.New("missing fields")
wrapped := NewErrorWrapper(400, errA, "bad data")
if errA == wrapped { // always false
// This code flow here will never be called
// because the value of struct errA (private in errors lib)
// is different than our wrapped error.
}
// or something worse like this
wrapWrapped := NewErrorWrapper(400, wrapped, "bad data")
if wrapWrapped == wrapped { // always false.
// wrapWrapped.Err is different than wrapped.Err
}
}
How to solve this problem?
errors
lib has an anonymous interface that we can implement.
It's anonymous definition is like this:
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
Look at this following snippet:
u, ok := err.(interface {
Unwrap() error
})
It's an anonymous interface that we can implement. This interface is used in errors
lib in Is and As functions.
These two functions will be used by us to handle equality checking, so we have to implement it for our wrapper.
// Implements the errors.Unwrap interface
func (err CustomErrorWrapper) Unwrap() error {
return err.Err // Returns inner error
}
The implementation will be used recursively by Is
and As
function until it cannot Unwrap
anymore.
So it doesn't really matter if CustomErrorWrapper
wraps another CustomErrorWrapper
, you can always get the root error cause as the wrapped CustomErrorWrapper
will be called to Unwrap
itself also.
Doing Equality Check on CustomErrorWrapper
Now let's do the equality check. We don't use the ==
syntax anymore. Instead we use the errors.Is
syntax.
var (
ErrSomething = errors.New("something happened")
)
func doSomething() error {
return ErrSomething
}
func theOneCallsDoSomething() error {
err := doSomething()
if err != nil {
return NewErrorWrapper(500, err, "something happened")
}
return nil
}
func main() {
err := theOneCallsDoSomething()
if errors.Is(err, ErrSomething) { // always false if err is nil
// handle ErrSomething error
}
}
But what if the error "shape" is a struct, like for example *json.SyntaxError?
type Foo struct {
Bar string `json:"bar"`
}
func repoData() (Foo, error) {
fake := []byte(`fake`)
var foo Foo
err := json.Unmarshal(fake, &foo)
if err != nil {
err = NewErrorWrapper(500, err, "failed to marshal json data")
}
return foo, err
}
func getDataFromRepo() (Foo, error) {
foo, err := repoData()
if err != nil {
return foo, NewErrorWrapper(500, err, "failed to get data from repo")
}
return foo, err
}
func main() {
foo, err := getDataFromRepo()
var syntaxError *json.SyntaxError
if errors.As(err, &syntaxError) {
fmt.Println(syntaxError.Offset) // Output: 3
}
// we could also check for CustomErrorWrapper
var ew CustomErrorWrapper
if errors.As(err, &ew) { // errors.As stop on first match
fmt.Println(ew.Message) // Output: failed to get data from repo
fmt.Println(ew.Code) // Output: 500
}
_ = foo
}
Notice how the syntax is used:
var syntaxError *json.SyntaxError
if errors.As(err, &syntaxError) {
It acts like how json.Unmarshal
would, but a little bit different. errors.As
requires pointer to a Value
that implements error
interface. Any otherway and it will panic.
The code snippet above includes check for CustomErrorWrapper
. But it only gets the first occurences or the outer most layer of wrapping.
// we could also check for CustomErrorWrapper
var ew CustomErrorWrapper
if errors.As(err, &ew) { // errors.As stop on first match
fmt.Println(ew.Message) // Output: failed to get data from repo
fmt.Println(ew.Code) // Output: 500
}
To get the lower wrapper message you have to do your own implementations. Like code snippet below:
// Returns the inner most CustomErrorWrapper
func (err CustomErrorWrapper) Dig() CustomErrorWrapper {
var ew CustomErrorWrapper
if errors.As(err.Err, &ew) {
// Recursively digs until wrapper error is not CustomErrorWrapper
return ew.Dig()
}
return err
}
And to actually use it:
var ew CustomErrorWrapper
if errors.As(err, &ew) { // errors.As stop on first match
fmt.Println(ew.Message) // Output: failed to get data from repo
fmt.Println(ew.Code) // Output: 500
// Dig for innermost CustomErrorWrapper
ew = ew.Dig()
fmt.Println(ew.Message) // Output: failed to marshal json data
fmt.Println(ew.Code) // Output: 400
}
Full Code Preview
Ok let's combine everything to get the full picture of how to create Error Wrapper.
package main
import (
"encoding/json"
"errors"
"fmt"
)
type CustomErrorWrapper struct {
Message string `json:"message"` // Human readable message for clients
Code int `json:"-"` // HTTP Status code. We use `-` to skip json marshaling.
Err error `json:"-"` // The original error. Same reason as above.
}
// Returns Message if Err is nil
func (err CustomErrorWrapper) Error() string {
if err.Err != nil {
return err.Err.Error()
}
return err.Message
}
func (err CustomErrorWrapper) Unwrap() error {
return err.Err // Returns inner error
}
// Returns the inner most CustomErrorWrapper
func (err CustomErrorWrapper) Dig() CustomErrorWrapper {
var ew CustomErrorWrapper
if errors.As(err.Err, &ew) {
// Recursively digs until wrapper error is not in which case it will stop
return ew.Dig()
}
return err
}
func NewErrorWrapper(code int, err error, message string) error {
return CustomErrorWrapper{
Message: message,
Code: code,
Err: err,
}
}
type Foo struct {
Bar string `json:"bar"`
}
func repoData() (Foo, error) {
fake := []byte(`fake`)
var foo Foo
err := json.Unmarshal(fake, &foo)
if err != nil {
err = NewErrorWrapper(400, err, "failed to marshal json data")
}
return foo, err
}
func getDataFromRepo() (Foo, error) {
foo, err := repoData()
if err != nil {
return foo, NewErrorWrapper(500, err, "failed to get data from repo")
}
return foo, err
}
func main() {
foo, err := getDataFromRepo()
var syntaxError *json.SyntaxError
// Get root error
if errors.As(err, &syntaxError) {
fmt.Println(syntaxError.Offset)
}
var ew CustomErrorWrapper
if errors.As(err, &ew) { // errors.As stop on first match
fmt.Println(ew.Message) // Output: failed to get data from repo
fmt.Println(ew.Code) // Output: 500
// Dig for innermost CustomErrorWrapper
ew = ew.Dig()
fmt.Println(ew.Message) // Output: failed to marshal json data
fmt.Println(ew.Code) // Output: 400
}
_ = foo
}
Error Wrapper in REST API Use Case
Let's use the CustomErrorWrapper
against something more concrete like HTTP REST Api to show flexibility of error wrapper to propagate information upstream.
First let's create Response Helpers
func ResponseError(rw http.ResponseWriter, err error) {
rw.Header().Set("Content-Type", "Application/json")
var ew CustomErrorWrapper
if errors.As(err, &ew) {
rw.WriteHeader(ew.Code)
log.Println(ew.Err.Error())
_ = json.NewEncoder(rw).Encode(ew)
return
}
// handle non CustomErrorWrapper types
rw.WriteHeader(500)
log.Println(err.Error())
_ = json.NewEncoder(rw).Encode(map[string]interface{}{
"message": err.Error(),
})
}
func ResponseSuccess(rw http.ResponseWriter, data interface{}) {
rw.Header().Set("Content-Type", "Application/json")
body := map[string]interface{}{
"data": data,
}
_ = json.NewEncoder(rw).Encode(body)
}
The one we interested in is ResponseError
. In the snippet:
var ew CustomErrorWrapper
if errors.As(err, &ew) {
rw.WriteHeader(ew.Code)
log.Println(ew.Err.Error())
_ = json.NewEncoder(rw).Encode(ew)
return
}
If the error is in fact a CustomErrorWrapper
, we can match the response code and message from the given CustomErrorWrapper
.
Then let's add server, handler code and repo simulation code.
var (
useSecondError = false
firstError = errors.New("first error")
secondError = errors.New("second error")
)
// Always error
func repoSimulation() error {
var err error
if useSecondError {
err = NewErrorWrapper(404, firstError, "data not found")
} else {
err = NewErrorWrapper(503, secondError, "required dependency are not available")
}
// This is for example and readability purposes! Don't follow this example. This is not thread / goroutine safe.
// Use Atomic Operations or Mutex for safe handling.
useSecondError = !useSecondError
return err
}
func handler(rw http.ResponseWriter, r *http.Request) {
err := repoSimulation()
if err != nil {
ResponseError(rw, err)
return
}
ResponseSuccess(rw, "this code should not be reachable")
}
func main() {
// Routes everything to handler
server := http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(handler),
}
log.Println("server is running on port 8080")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
The repoSimulation
is simple. It will (in its bad practice glory) alternate returned error.
If we add everything, it will look like this:
Full Code Preview HTTP Service
package main
import (
"encoding/json"
"errors"
"log"
"net/http"
)
type CustomErrorWrapper struct {
Message string `json:"message"` // Human readable message for clients
Code int `json:"-"` // HTTP Status code. We use `-` to skip json marshaling.
Err error `json:"-"` // The original error. Same reason as above.
}
// Returns Message if Err is nil
func (err CustomErrorWrapper) Error() string {
if err.Err != nil {
return err.Err.Error()
}
return err.Message
}
func (err CustomErrorWrapper) Unwrap() error {
return err.Err // Returns inner error
}
// Returns the inner most CustomErrorWrapper
func (err CustomErrorWrapper) Dig() CustomErrorWrapper {
var ew CustomErrorWrapper
if errors.As(err.Err, &ew) {
// Recursively digs until wrapper error is not CustomErrorWrapper
return ew.Dig()
}
return err
}
func NewErrorWrapper(code int, err error, message string) error {
return CustomErrorWrapper{
Message: message,
Code: code,
Err: err,
}
}
// ===================================== Simulation =====================================
var (
useSecondError = false
firstError = errors.New("first error")
secondError = errors.New("second error")
)
// Always error
func repoSimulation() error {
var err error
if useSecondError {
err = NewErrorWrapper(404, firstError, "data not found")
} else {
err = NewErrorWrapper(503, secondError, "required dependency are not available")
}
// This is for example and readability purposes! Don't follow this example. This is not thread / goroutine safe.
// Use Atomic Operations or Mutex for safe handling.
useSecondError = !useSecondError
return err
}
func handler(rw http.ResponseWriter, r *http.Request) {
err := repoSimulation()
if err != nil {
ResponseError(rw, err)
return
}
ResponseSuccess(rw, "this code should not be reachable")
}
func ResponseError(rw http.ResponseWriter, err error) {
rw.Header().Set("Content-Type", "Application/json")
var ew CustomErrorWrapper
if errors.As(err, &ew) {
rw.WriteHeader(ew.Code)
log.Println(ew.Err.Error())
_ = json.NewEncoder(rw).Encode(ew)
return
}
// handle non CustomErrorWrapper types
rw.WriteHeader(500)
log.Println(err.Error())
_ = json.NewEncoder(rw).Encode(map[string]interface{}{
"message": err.Error(),
})
}
func ResponseSuccess(rw http.ResponseWriter, data interface{}) {
rw.Header().Set("Content-Type", "Application/json")
body := map[string]interface{}{
"data": data,
}
_ = json.NewEncoder(rw).Encode(body)
}
func main() {
// Routes everything to handler
server := http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(handler),
}
log.Println("server is running on port 8080")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
Compile the code and run, and it will print the server is running on port 8080.
Run this command repeatedly in different terminal.
curl -sSL -D - localhost:8080
You will get different message every time with different response code. With the logger only showing private error message not shown to client.
This may seem simple, but it's very extensible, scalable, and can be created as flexible or frigid as you like.
For example, You can integrate runtime.Frame
to get the location of whoever called NewErrorWrapper
for easier debugging.
Top comments (1)
Thanks for this. Well written and lots of information I hadn't read about. Thanks again!