Go has a unique stance on errors by treating them as values. There are no such constructs as exceptions in the language, although you may treat errors in this manner. Dealing with errors can sometimes feel cumbersome and verbose. However, in Go 1.13 the introduction of Wrapping and Unwrapping errors was added to the language. This extends errors to embed errors within themselves, creating a nest of errors that you can interact with.

Wrapping + Unwrapping errors

Wrapping errors allows you to add more context around your error messages. This is achieved by embedding error types or error objects within your errors. This allows for greater flexibility (and reduction of code, as you will later see) to check if the error you received of a specific type/object without needing to parse the error message.

Creating a wrapped error can be done by calling fmt.Errorf() and using %w within.

fmt.Errorf("error %s: %w", "my error", err)

A quick example of wrapping an error and then unwrapping it

package main

import (
 "errors"
 "fmt"
)

func main() {
 var errrorInner = errors.New("inner error")
 wrapped := fmt.Errorf("outer error: %w", errrorInner)
 // Prints out the entire error message
 fmt.Println(wrapped)
 // Prints out the wrapped portion of the error message which is %w
 fmt.Println(errors.Unwrap(wrapped))
 // Prints out nil as we only wrapped a single error
 fmt.Println(errors.Unwrap(errors.Unwrap(wrapped)))
}

Output:

outer error: inner error
inner error
<nil>

You can see that there are two parts to the error when we wrap/unwrap it.

  • the initial error layer: outer message: %w
  • the embedded %w layer: inner error

It is also possible to check equality if the wrapped error matches a specific error by doing something such as this.

package main

import (
 "errors"
 "fmt"
)

func main() {
 var errrorInner = errors.New("inner error")
 wrapped := fmt.Errorf("outer error: %w", errrorInner)
 if wrapped == errrorInner {
  fmt.Println("I won't execute")
 }
 if errors.Unwrap(wrapped) == errrorInner {
  fmt.Println(errors.Unwrap(wrapped))
 }
}

Output:

go run main.go
inner error

While you can check to see if a specific layer of the error matches this can be cumbersome. Imagine having multiple errors wrapped within itself. This is where errors.Is() and errors.Has() can be used to simplify your code.

Is()

The errors.Is() checks to see if any error in nest of errors matches a specified target. This can drastically cut down the need to repeatedly call Unwrap and doing a comparison check if you have nested errors.

The same code that we had above where we had to unwrap and check the error can be reduced down to the following.

package main

import (
 "errors"
 "fmt"
)

func main() {
 var errrorInner = errors.New("inner error")
 wrapped := fmt.Errorf("outer error: %w", errrorInner)
 if errors.Is(wrapped, errrorInner) {
  fmt.Println(errors.Unwrap(wrapped))
 }
}

Now, you will still be required to unwrap the error however many times you have it nested to get that specific message.

Lets take our same code but add another outer layer of error and then unwrap accordingly.

package main

import (
 "errors"
 "fmt"
)
func main() {
 var errrorInner = errors.New("inner error")
 wrapped := fmt.Errorf("outer error: %w", errrorInner)
 start := fmt.Errorf("this is by base %w", wrapped)
 fmt.Println(start)
 if errors.Is(start, errrorInner) {
  oneUnwrap := errors.Unwrap(start)
  fmt.Println(oneUnwrap)

  twoUnwrap := errors.Unwrap(oneUnwrap)
  fmt.Println(twoUnwrap)
 }
}

Output:

go run main.go
this is by base outer error: inner error
outer error: inner error
inner error

As()

While similar to Is() the biggest difference here is that As() checks the error nesting for a specific type . This can be viewed as a cleaner way to do type assertion if e, ok := err.(*ErrorInner); ok

Here is an example:

package main
import (
 "errors"
 "fmt"
)
type ErrorInner struct {
 msg string
}
func (e *ErrorInner) Error() string {
 return fmt.Sprintf("message: %s", e.msg)
}
func main() {
 wrapped := fmt.Errorf("outer error: %w", &ErrorInner{msg: "inner error"})
 fmt.Println(wrapped)
 var targetErr *ErrorInner
 if errors.As(wrapped, &targetErr) {
  fmt.Println(errors.Unwrap(wrapped))
 }
}

Output:

go run main.go
outer error: message: inner error
message: inner error

In the example the full wrapped error had included the Error() method from ErrorInner . Then when we want to call As() we need to pass in a pointer of the type we are looking for.

Wrapping up (pun intended)

The ability to wrap errors is quite useful. As you can have multiple “failure” states in a function. Creating custom errors for each of those states and then having a single check with Is() or As() can drastically cut down on code. It can also help to cleaner and clearer code as the additional contextual information. Useful Links