Error Wrapping
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.
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8var noRows = errors.New("no rows found")
9
10func getRecord() error {
11 return noRows
12}
13
14func webService() error {
15 if err := getRecord(); err != nil {
16 return fmt.Errorf("Error %s when calling DB", err)
17 }
18 return nil
19}
20
21func main() {
22 if err := webService(); err != nil {
23 fmt.Printf("Error: %s when calling webservice\n", err)
24 return
25 }
26 fmt.Println("webservice call successful")
27
28}
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.
1func main() {
2 if err := webService(); err != nil {
3 if errors.Is(err, noRows) {
4 fmt.Printf("The searched record cannot be found. Error returned from DB is %s", err)
5 return
6 }
7 fmt.Println("unknown error when searching record")
8 return
9
10 }
11 fmt.Println("webservice call successful")
12
13}
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
1 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.
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8var noRows = errors.New("no rows found")
9
10func getRecord() error {
11 return noRows
12}
13
14func webService() error {
15 if err := getRecord(); err != nil {
16 return fmt.Errorf("Error %w when calling DB", err)
17 }
18 return nil
19}
20
21func main() {
22 if err := webService(); err != nil {
23 if errors.Is(err, noRows) {
24 fmt.Printf("The searched record cannot be found. Error returned from DB is %s", err)
25 return
26 }
27 fmt.Println("unknown error when searching record")
28 return
29
30 }
31 fmt.Println("webservice call successful")
32
33}
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 :)
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8type DBError struct {
9 desc string
10}
11
12func (dbError DBError) Error() string {
13 return dbError.desc
14}
15
16func getRecord() error {
17 return DBError{
18 desc: "no rows found",
19 }
20}
21
22func webService() error {
23 if err := getRecord(); err != nil {
24 return fmt.Errorf("Error %w when calling DB", err)
25 }
26 return nil
27}
28
29func main() {
30 if err := webService(); err != nil {
31 var dbError DBError
32 if errors.As(err, &dbError) {
33 fmt.Printf("The searched record cannot be found. Error returned from DB is %s", dbError)
34 return
35 }
36 fmt.Println("unknown error when searching record")
37 return
38
39 }
40 fmt.Println("webservice call successful")
41
42}
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 liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter and LinkedIn. Have a good day.