DEV Community

Tigor
Tigor

Posted on

Go: Creating Custom Error Wrapper and Do Proper Error Equality Check

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
}
Enter fullscreen mode Exit fullscreen mode

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{}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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{}
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

Look at this following snippet:

u, ok := err.(interface {
    Unwrap() error
})
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Notice how the syntax is used:

var syntaxError *json.SyntaxError
if errors.As(err, &syntaxError) {
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (1)

Collapse
soulsbane profile image
Paul Crane

Thanks for this. Well written and lots of information I hadn't read about. Thanks again!