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:
err
field with the typeerror
(this will be the inner/wrapped error)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}
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