Golang error warp

Errors are a core part of almost every programming language, and how we handle them is a critical part of software development. One of the things that I really enjoy about programming in Go is the implementation of errors and how they are treated: Effective without having unnecessary complexity. This blog post will dive into what errors are and how they can be wrapped (and unwrapped).

Errors are a core part of almost every programming language, and how we handle them is a critical part of software development. One of the things that I really enjoy about programming in Go is the implementation of errors and how they are treated: Effective without having unnecessary complexity. This blog post will dive into what errors are and how they can be wrapped (and unwrapped).

What are errors?

Let’s start from the beginning. It’s common to see an error getting returned and handled from a function:

1
2
3
func myFunction() error {
    // ...
}

But what exactly is an error? It is one of the simplest interfaces defined in Go (source code reference):

1
2
3
type error interface {
	Error() string
}

It has a single function Error that takes no parameters and returns a string. That’s it! That’s all there is to implementing the error interface. We’ll see later on how we can implement error to create our own custom error types.

Creating errors

Most of the time you’ll rely on creating errors through one of two ways:

1
fmt.Errorf("error doing something")

Or:

1
errors.New("error doing something")

The former is used when you want to use formatting with the typical fmt verbs. If you aren’t wrapping an error (more on this below) then fmt.Errorf effectively makes a call to errors.New (source code reference). So if you’re not wrapping an error or using any additional formatting then it’s a personal preference.

What do these non-wrapped errors look like? Breaking into the debugger we can analyze them:

1
2
3
(dlv) p err1
error(*errors.errorString) *{
        s: "error doing something",}

The concrete type is *errors.errorString. Let’s take a look at this Go struct in the errors package (source code reference):

1
2
3
4
5
6
7
type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

errorString is a simple implementation having a single string field and because this implements the error interface it defines the Error function, which just returns the struct’s string.

Creating custom error types

What if we want to create custom errors with certain pieces of information? Now that we understand what an error actually is (by implementing the error interface) we can define our own:

1
2
3
4
5
6
7
8
type myCustomError struct {
    errorMessage    string
    someRandomValue int
}

func (e *myCustomError) Error() string {
    return fmt.Sprintf("Message: %s - Random value: %d", e.errorMessage, e.someRandomValue)
}

And we can use them just like any other error in Go:

1
2
3
4
5
6
7
8
9
10
11
func someFunction() error {
    return &myCustomError{
        errorMessage:    "hello world",
        someRandomValue: 13,
    }
}

func main() {
    err := someFunction()
    fmt.Printf("%v", err)
}

The output of running this code is expected:

1
Message: hello world - Random value: 13

Error wrapping

In Go it is common to return errors and then keep bubbling that up until it is handled properly (exiting, logging, etc.). Consider this example:

1
2
3
4
5
6
7
8
9
10
11
12
13
func doAnotherThing() error {
    return errors.New("error doing another thing")
}

func doSomething() error {
    err := doAnotherThing()
    return fmt.Errorf("error doing something: %v", err)
}

func main() {
    err := doSomething()
    fmt.Println(err)
}

main makes a call to doSomething, which calls doAnotherThing and takes its error.

Note: It’s common to have error handling with if err != nil ... but I wanted to keep this example as small as possible.

Typically you want to preserve the context of your inner errors (in this case “error doing another thing”) so you might try to do a superficial wrap with fmt.Errorf and the %v verb. In fact, the output seems reasonable:

1
error doing something: error doing another thing

But outside of wrapping the error messages, we’ve lost the inner errors themselves effectively. If we were to analyze err in main, we’d see this:

1
2
3
(dlv) p err
error(*errors.errorString) *{
        s: "error doing something: error doing another thing",}

Most of the time that is typically fine. A superficially wrapper error is ok for logging and troubleshooting. But what happens if you need to programmatically test for a particular error or treat an error as a custom one? With the above approach, that is extremely complicated and error-prone.

The solution to that challenge is by wrapping your errors. To wrap your errors you would use fmt.Errorf with the %w verb. Let’s modify the single line of code in the above example:

1
return fmt.Errorf("error doing something: %w", err)

Now let’s inspect the returned error in main:

1
2
3
4
5
(dlv) p err
error(*fmt.wrapError) *{
        msg: "error doing something: error doing another thing",
        err: error(*errors.errorString) *{
                s: "error doing another thing",},}

We’re no longer getting the type *errors.errorString. Now we have the type *fmt.wrapError. Let’s take a look at how Go defines wrapError (source code reference):

1
2
3
4
5
6
7
8
9
10
11
12
type wrapError struct {
	msg string
	err error
}

func (e *wrapError) Error() string {
	return e.msg
}

func (e *wrapError) Unwrap() error {
	return e.err
}

This adds a couple of new things:

  1. err field with the type error (this will be the inner/wrapped error)
  2. Unwrap method that gives us access to the inner/wrapped error

This extra wiring gives us a lot of powerful capabilities when dealing with wrapped errors.

Error equality

One of the scenarios that error wrapping unlocks is a really elegant way to test if an error or any inner/wrapped errors are a particular error. We can do that with the errors.Is function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var errAnotherThing = errors.New("error doing another thing")

func doAnotherThing() error {
    return errAnotherThing
}

func doSomething() error {
    err := doAnotherThing()
    return fmt.Errorf("error doing something: %w", err)
}

func main() {
    err := doSomething()

    if errors.Is(err, errAnotherThing) {
        fmt.Println("Found error!")
    }

    fmt.Println(err)
}

I changed the code of doAnotherThing to return a particular error (errAnotherThing). Even though this error gets wrapped in doSomething, we’re still able to concisely test if the returned error is or wraps errAnotherThing with errors.Is.

errors.Is essentially just loops through the different layers of the error and unwraps, testing to see if it is equal to the target error (source code reference).

Specific error handling

Another scenario is if you have a particular type of error that you want to handle, even if it is wrapped. Using a variation of an earlier example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type myCustomError struct {
    errorMessage    string
    someRandomValue int
}

func (e *myCustomError) Error() string {
    return fmt.Sprintf("Message: %s - Random value: %d", e.errorMessage, e.someRandomValue)
}

func doAnotherThing() error {
    return &myCustomError{
        errorMessage:    "hello world",
        someRandomValue: 13,
    }
}

func doSomething() error {
    err := doAnotherThing()
    return fmt.Errorf("error doing something: %w", err)
}

func main() {
    err := doSomething()

    var customError *myCustomError
    if errors.As(err, &customError) {
        fmt.Printf("Custom error random value: %dn", customError.someRandomValue)
    }

    fmt.Println(err)
}

This allows us to handle err in main if it (or any wrapped errors) have a concrete type of *myCustomError. The output of running this code:

1
2
Custom error random value: 13
error doing something: Message: hello world - Random value: 13

Summary

Understanding how errors and error wrapping in Go can go a really long way in implementing them in the best possible way. Using them “the Go way” can lead to code that is easier to maintain and troubleshoot. Enjoy!

Welcome to tutorial no. 32 in our Golang tutorial series.

In this tutorial we will learn about error wrapping in Go and why do we even need error wrapping. Let’s get started.

What is error wrapping?

Error wrapping is the process of encapsulating one error into another. Let’s say we have a web server which accesses a database and tries to fetch a record from the DB. If the database call returns an error, we can decide whether to wrap this error or send our own custom error from the webservice. Let’s write a small program to understand this.

package main

import (  
    "errors"
    "fmt"
)

var noRows = errors.New("no rows found")

func getRecord() error {  
    return noRows
}

func webService() error {  
    if err := getRecord(); err != nil {
        return fmt.Errorf("Error %s when calling DB", err)
    }
    return nil
}

func main() {  
    if err := webService(); err != nil {
        fmt.Printf("Error: %s when calling webservicen", err)
        return
    }
    fmt.Println("webservice call successful")

}

Run in playground

In the above program, in line no. 16, we send the string description of the error that occurs when making the getRecord function call. While this may actually seem like error wrapping, it is not :). Let’s understand how to wrap errors in the next section.

Error wrapping and the Is function

The Is function in the errors package reports whether any of the errors in the chain matches the target. In our case, we return noRows error from the getRecord function in line no. 11. The string format of this error is returned from the webService function in line no. 16. Let’s modify the main function of this program and use the Is function to check whether any error in the chain matches the noRows error.

func main() {  
    if err := webService(); err != nil {
        if errors.Is(err, noRows) {
            fmt.Printf("The searched record cannot be found. Error returned from DB is %s", err)
            return
        }
        fmt.Println("unknown error when searching record")
        return

    }
    fmt.Println("webservice call successful")

}

In the above main function, in line no. 3, the Is function will check whether any error in the error chain that err holds
contains a noRows error. The code in it’s current state won’t work and the if condition in line no. 3 of the above main function will fail. To make it work, we need to wrap the noRows error when it is returned from the webService function. One way to do this is to use the %w format specifier when returning the error instead of %s. So if we modify the line returning the error to

        return fmt.Errorf("Error %w when calling DB", err)

it means that the newly returned error wraps the original noRows and the if condition in line no. 3 of the above main function will succeed. The complete program with the error wrapping is provided below.

package main

import (  
    "errors"
    "fmt"
)

var noRows = errors.New("no rows found")

func getRecord() error {  
    return noRows
}

func webService() error {  
    if err := getRecord(); err != nil {
        return fmt.Errorf("Error %w when calling DB", err)
    }
    return nil
}

func main() {  
    if err := webService(); err != nil {
        if errors.Is(err, noRows) {
            fmt.Printf("The searched record cannot be found. Error returned from DB is %s", err)
            return
        }
        fmt.Println("unknown error when searching record")
        return

    }
    fmt.Println("webservice call successful")

}

Run in playground

When this program is run, it will print.

The searched record cannot be found. Error returned from DB is Error no rows found when calling DB  

As function

The As in the errors package will try to convert the error that is passed as input to the target error type. It will succeed if any of the error in the error chain matches the target. If it’s successful it will return true, and it will set the target to the first error in the error chain that matches. A program will make things easier to understand :)

package main

import (  
    "errors"
    "fmt"
)

type DBError struct {  
    desc string
}

func (dbError DBError) Error() string {  
    return dbError.desc
}

func getRecord() error {  
    return DBError{
        desc: "no rows found",
    }
}

func webService() error {  
    if err := getRecord(); err != nil {
        return fmt.Errorf("Error %w when calling DB", err)
    }
    return nil
}

func main() {  
    if err := webService(); err != nil {
        var dbError DBError
        if errors.As(err, &dbError) {
            fmt.Printf("The searched record cannot be found. Error returned from DB is %s", dbError)
            return
        }
        fmt.Println("unknown error when searching record")
        return

    }
    fmt.Println("webservice call successful")

}

Run in playground

In the above program, we have modified the getRecord function in line no. 16 to return a custom error of type DBError.

In line no. 32 in the main function, we try to convert the error returned from the webService() function call to the type DBError. The if statement in line no. 32 will succeed since we have wrapped the DBError when returning error from the webService() function in line no. 24. Running this program will print

The searched record cannot be found. Error returned from DB is no rows found  

Should we wrap errors?

The answer to this question is, it depends. If we wrap the error, we are exposing it to callers of our library/function. We generally do not want to wrap errors which contain the internal implementation details of a function. One more important thing to remember is, if we return a wrapped error and later decide to remove the error wrapping, the code which uses our library will start failing. So wrapped errors should be considered as part of the API and appropriate version changes should be done if we decide to modify the error that we return.

I hope you like this tutorial. Have a great day :)

In go, error can wrap another error as well.  What does the wrapping of error mean? It means to create a hierarchy of errors in which a  particular instance of error wraps another error and that particular instance itself can be wrapped inside another error.  Below is the syntax for wrapping an error

e := fmt.Errorf("... %w ...", ..., err, ...)

%w directive Is used for wrapping the error.  The fmt.Errorf should be called with only one %w directive. Let’s see an example.

package main

import (
	"fmt"
)

type errorOne struct{}

func (e errorOne) Error() string {
	return "Error One happended"
}

func main() {

	e1 := errorOne{}

	e2 := fmt.Errorf("E2: %w", e1)

	e3 := fmt.Errorf("E3: %w", e2)

	fmt.Println(e2)

	fmt.Println(e3)

}

Output

E2: Error One happended
E3: E2: Error One happended

In the above program, we created a struct errorOne that has an Error method hence it implements the error interface. Then we created an instance of the errorOne struct named e1. Then we wrapped that instance e1 into another error e2 like this

e2 := fmt.Errorf("E2: %w", e1)

Then we wrapped e2 into e3 like below. 

e3 := fmt.Errorf("E3: %w", e2)

So so we created a hierarchy of errors in which e3 wraps e2 and e2 wraps e1.  Thus e3 also wraps e1 transitively. When we print e2  it also prints the error from e1 and gives the output.

E2: Error One happended

When we print e3 it prints the error from e2 as well as e1 and gives the output.

E3: E2: Error One happended

Now the question which comes to the mind that whats the use case of wrapping the errors. To understand it let’s see an example

package main

import (
	"fmt"
)

type notPositive struct {
	num int
}

func (e notPositive) Error() string {
	return fmt.Sprintf("checkPositive: Given number %d is not a positive number", e.num)
}

type notEven struct {
	num int
}

func (e notEven) Error() string {
	return fmt.Sprintf("checkEven: Given number %d is not an even number", e.num)
}

func checkPositive(num int) error {
	if num < 0 {
		return notPositive{num: num}
	}
	return nil
}

func checkEven(num int) error {
	if num%2 == 1 {
		return notEven{num: num}
	}
	return nil
}

func checkPostiveAndEven(num int) error {
	if num > 100 {
		return fmt.Errorf("checkPostiveAndEven: Number %d is greater than 100", num)
	}

	err := checkPositive(num)
	if err != nil {
		return err
	}

	err = checkEven(num)
	if err != nil {
		return err
	}

	return nil
}

func main() {
	num := 3
	err := checkPostiveAndEven(num)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Givennnumber is positive and even")
	}

}

Output

checkEven: Given number 3 is not an even number

In the above program, we have a function checkPostiveAndEven that checks whether a number is even and positive. In turn, it calls the checkEven function to check if the number is even. And then it calls checkPositive function to check if the number is positive. If a number is not even and positive it an error is raised.

In the above program, it is impossible to tell stack trace of the error. We know that this error came from checkEven function for the above output. But which function called the checkEven function is not clear from the error. This is where wrapping the error comes in the picture.  This becomes more useful when the project is big and there are a lot of functions calling each other.  Let’s rewrite the program by wrapping the error.

package main

import (
	"fmt"
)

type notPositive struct {
	num int
}

func (e notPositive) Error() string {
	return fmt.Sprintf("checkPositive: Given number %d is not a positive number", e.num)
}

type notEven struct {
	num int
}

func (e notEven) Error() string {
	return fmt.Sprintf("checkEven: Given number %d is not an even number", e.num)
}

func checkPositive(num int) error {
	if num < 0 {
		return notPositive{num: num}
	}
	return nil
}

func checkEven(num int) error {
	if num%2 == 1 {
		return notEven{num: num}
	}
	return nil
}

func checkPostiveAndEven(num int) error {
	if num > 100 {
		return fmt.Errorf("checkPostiveAndEven: Number %d is greater than 100", num)
	}

	err := checkPositive(num)
	if err != nil {
		return fmt.Errorf("checkPostiveAndEven: %w", err)
	}

	err = checkEven(num)
	if err != nil {
		return fmt.Errorf("checkPostiveAndEven: %w", err)
	}

	return nil
}

func main() {
	num := 3
	err := checkPostiveAndEven(num)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Given number is positive and even")
	}

}

Output

checkPostiveAndEven: checkEven: Given number 3 is not an even number

 The above program is same as the previous program just that in the checkPostiveAndEven function , we wrap the errors like below.

fmt.Errorf("checkPostiveAndEven: %w", err)

So the output is more clear and the error is more informative. The output clearly mentions the sequence of calling as well

checkPostiveAndEven: checkEven: Given number 3 is not an even number

Unwrap an error

In the above section, we studied about wrapping the error. It is also possible to unwrap the error. Unwrap function of errors package can be used to unwrap an error. Below is the syntax of the function.

func Unwrap(err error) error

If the err wraps another error, then the wrapped error will be returned otherwise Unwrap function will return nil.

Let’s see a program to illustrate the same

import (
    "errors"
    "fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
    return "Error One happended"
}
func main() {
    e1 := errorOne{}
    e2 := fmt.Errorf("E2: %w", e1)
    e3 := fmt.Errorf("E3: %w", e2)
    fmt.Println(errors.Unwrap(e3))
    fmt.Println(errors.Unwrap(e2))
    fmt.Println(errors.Unwrap(e1))
}

Output

E2: Error One happended
Error One happended

In the above program, we created a struct errorOne that has an Error method hence it implements the error interface. Then we created an instance of the errorOne struct named e1. Then we wrapped that instance e1 into another error e2 like this

e2 := fmt.Errorf("E2: %w", e1)

Then we wrapped e2 into e3 like below. 

e3 := fmt.Errorf("E3: %w", e2)

Hence

fmt.Println(errors.Unwrap(e3))

will return wrapped error e2, as e3 wraps e2 and output will be

E2: Error One happended

Also,

fmt.Println(errors.Unwrap(e2))

will return wrapped error e1 as e2 further wraps e1 and output will be

Error One happened

While

fmt.Println(errors.Unwrap(e1))

will output nil as e1 does not wraps any error

{nil}
  • go
  • golang
  • 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.

    In this article, we shall be discussing how to return and handle errors effectively using custom and inbuilt Golang functions, with help of practical examples.

    Golang return error

    An error is basically a fault that occurs in a program execution flow. These errors can be of various natures:- Caused by programmers through code syntax and interface errors , system-related Resources and Runtime errors, algorithm-related logic and arithmetic errors. Which later are solved through debugging process.

    In Golang ,The Error is an interface that holds Error() string method. Its implemented as follows

    type error interface {
    Error() string
    }
    

    In an nutshell, when the Error() method is called, it’s return value is in form of string datatype. Through the use of inbuilt Go functions of the fmt and errors packages, we can construct the kind of error message to be displayed. Below is an example to construct Errors using fmt.Error() in Golang, i.e you want to read a file from a given path, unfortunate the file doesn’t exist or the path given is invalid. For example:=

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func ReadFile(file string) error {
    	dataFile, err := os.ReadFile(file)
    	if err != nil {
    		return fmt.Errorf("An error occurred while Reading the file: open : %v", err)
    	}
    	fmt.Println(string(dataFile))
    	return nil
    }
    func main() {
    	resultsErr := ReadFile("")
    	if resultsErr != nil {
    		fmt.Printf("%v", resultsErr)
    	}
    }
    

    Output:

    ALSO READ: Golang check if key exists in map [SOLVED]

    With the file attached ensure you replace the ReadFile(«test.txt»)

    $ go run main.go
    Hello

    without file attached

    $ go run main.go
    An error occurred while Reading the file: open: no such file or directory

    Explanation:- In the above code, ReadFile() error{} function returns an error which is nil whenever no error encountered. In Golang, the Error return value is nil as the default, or “zero”. Notice that checking if err != nil{} is the idiomatic way to determine if an error was encountered in Golang syntax, in this function we are returning the error only, handling the file data within the function. fmt.Error() enables us to customize the kind of message to be displayed. These messages are always in a lowercase format and don’t end with punctuation.

    In Golang there are numerous ways to return and handle errors Namely:=

    • Casting Errors
    • Error wrapping mechanism
    • Panic, defer and recover

    Different methods of error handling in Go Func

    Method 1:- Casting Errors

    Casting errors is a way of defining custom and expected errors, with golang we can make use of erros.Isand errors.As() error functions to cast different types of errors. i.e,errors.Is we create a custom error of a particular type and check If the error matches the specific type the function will return true, if not it will return false.

    package main
    
    import (
    	"errors"
    	"fmt"
    	"io/fs"
    	"os"
    )
    
    var fileNotFound = errors.New("The file doesn't  exist")
    
    func ReadFile(file string) error {
    	dataFile, readErr := os.ReadFile(file)
    	if readErr != nil {
    		if errors.Is(readErr, fs.ErrNotExist) {
    			return fmt.Errorf("this fileName %s  doesn't exist ", file)
    		} else {
    			return fmt.Errorf("Error  occured while opening the file : %w", readErr)
    		}
    	}
    	fmt.Println(string(dataFile))
    	return nil
    }
    func main() {
    	fileName := os.Args[1]
    	if fileName != "" {
    		resultsError := ReadFile(fileName)
    		if resultsError != nil {
    			fmt.Printf("%v", resultsError)
    		}
    	} else {
    		fmt.Println("the file name cant be empty")
    	}
    }
    

    Output:

    $ go run main.go "new"
    this fileName new  doesn't exist

    Explanation:- We are using errors.Is(readErr, fs.ErrNotExist) {} to check if the file passed exist, if it doesn’t exist we return custom message as shown above. we can also use the custom error message such as errors.New() to create expected error and handle it as errors.Is(readErr, fileNotFound) {} the return values will be the same.

    ALSO READ: Golang Print Struct Variables [SOLVED]

    Method 2:- Error wrapping

    Wrapping is a way of using other errors within a function to provide more context and detailed error messages.

    fmt.Error() function enable us to create a wrapped errors with use of %w flag. The %w flag is used for inspecting and unwrapping errors.
    In this subtitles we can incorporate other functions from errors package used to handle errors, namely:- errors.As, errors.Is, errors.Unwrap functions. errors.As is used to cast a specific error type, i.e func As(err error, target any) bool{}, also, the errors.Unwrap is used to inspect and expose the underlying errors in a program,i.e func (e *PathError)Unwrap()error{ return e.Err}, Furthermore the errors.Is mostly for comparing error value against the sentinel value if is true or false, i.e func Is(err,target error) bool{}.

    Example of Error Wrapping

    package main
    
    import (
    	"errors"
    	"fmt"
    	"os"
    )
    
    func ReadFile(file string) error {
    	dataFile, readErr := os.ReadFile(file)
    	var pathError *os.PathError
    	if readErr != nil {
    		if errors.As(readErr, &pathError) {
    			return fmt.Errorf("this fileName %s  doesn't exist and failed  opening file at this path %v", file, pathError.Path)
    		}
    		return fmt.Errorf("Error  occured while opening the file : %w", readErr)
    
    	}
    	fmt.Println(string(dataFile))
    	return nil
    }
    func main() {
    	fileName := os.Args[1]
    	if fileName != "" {
    		resultsError := ReadFile(fileName)
    		if resultsError != nil {
    			fmt.Printf("%v", resultsError)
    		}
    	} else {
    		fmt.Println("the file name can't be empty")
    	}
    }
    

    Output:

    With the file attached ensure you replace the ReadFile(«test.txt»)

    $ go run main.go
    Hello

    without file attached

    $ go run main.go ""
    the file name can't be empty
    $ go run main.go next.txt
    this fileName news.txt  doesn't exist and failed opening file at this path news.txt

    Explanation:- In the above code we have used fmt.Errorf() functions to format the error message to be displayed and wrapping error using a custom error message with wrap function errors.Is() which checks if the path exists. You can avoid unnecessary error wrapping and handle it once.

    ALSO READ: Golang SQLite3 Tutorial [With Examples]

    Method-3: Using Panic, Defer and Recover

    We have covered this topic in detail in a separate article Golang panic handing [capture, defer, recover, log]

    Summary

    At this point in this article, you have learned various ways to return and handle errors in the Golang function. In Go, Errors are considered to be a very lightweight piece of data that implements the Error interface. Custom errors in Go help in debugging and signaling where the error has occurred from. Error tracing is easy as compared to other programming languages. Golang application development, error handling is very critical and helps one not only during debugging but also to monitor the application behavior. We recommend you to read more about panic, recover and defer mechanism of error handling as well.

    References

    error-handling in Go
    Working with errors in golang
    Errors

    Понравилась статья? Поделить с друзьями:
  • Golang error unwrap
  • Golang error types
  • Golang error stack trace
  • Golang error message
  • Golang error interface