Skip to content

Commit 3d0c2c3

Browse files
authored
Concept: Error Wrapping (exercism#2715)
1 parent 05cf154 commit 3d0c2c3

File tree

5 files changed

+386
-0
lines changed

5 files changed

+386
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"blurb": "Go allows to add context to errors and provides functions to check for or retrieve the original error later on. This is called error wrapping (and unwrapping).",
3+
"authors": ["junedev"],
4+
"contributors": []
5+
}

concepts/error-wrapping/about.md

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# About
2+
3+
## Adding context to errors
4+
5+
We explored basic error handling in Go in the [errors concept][concept-errors].
6+
As you learned there, by default errors do not carry around stack traces.
7+
That makes it crucial to ensure the error itself contains enough information to identify the problem.
8+
9+
If we wanted to add information to an existing error with the tools we already know, we could write something like this to create a new error with the combined text of the original error and the additional information:
10+
11+
```go
12+
err := errors.New(fmt.Sprintf("parsing age for user %d: %v", userID, originalError))
13+
```
14+
15+
Luckily, the `fmt` package from the standard library contains a short-hand version for this in form of the `Errorf` function.
16+
That allows you, for example, to easily add information about the context in which the error occurred.
17+
18+
```go
19+
originalError := errors.New("unexpected negative number")
20+
userID := 123
21+
err := fmt.Errorf("parsing age for user %d: %v", userID, originalError)
22+
err.Error()
23+
// => "parsing age for user 123: unexpected negative number"
24+
```
25+
26+
Unless you are sure that the error already has enough information included, you should try to get into the habit of always adding context to an error before returning it from a function.
27+
That means your standard error handling pattern should look like this:
28+
29+
```go
30+
result, err := calculateSomething()
31+
if err != nil {
32+
return 0, fmt.Errorf("calculating something: %v", err)
33+
}
34+
```
35+
36+
As shown in the examples, the convention is to separate the error from the added message with a colon.
37+
Just like the message of the original error, also the context message should not be capitalized and no period should be added at the end.
38+
39+
~~~~exercism/note
40+
When adding context to an error, it is tempting to start the message with "failed to ..." or similar.
41+
However, as an error can accumulate a lot of additional context as it travels up the function chain, this can make the final message that will be logged or the user will see harder to read/ full of duplication.
42+
43+
Here is an example to illustrate:
44+
45+
```text
46+
failed to validate request: failed to parse path parameter: failed to convert number: invalid input "abc"
47+
vs.
48+
validating request: parsing path parameter: converting to number: invalid input "abc"
49+
```
50+
51+
So keep this end result in mind when writing the context messages and refrain from adding text about the fact that something did not go as expected.
52+
This is usually clear from the context (e.g. log level "ERROR") and the original error message.
53+
~~~~
54+
55+
Often this way of adding information for an error is good enough but there are cases where you want to allow the consumer of your error to check for or retrieve the original underlying error.
56+
Adding context in way that allows this is called "wrapping an error" in Go.
57+
58+
## Wrapping errors and checking for sentinel errors
59+
60+
Error wrapping can be achieved with a very minor change to the code we saw above.
61+
To wrap an error, you need to use the formatting verb `%w` instead of `%v`.
62+
Behind the scenes, this will make sure that the resulting error implements an `Unwrap` method which returns the original error.
63+
Because of that, then `errors.Is` can be used to check whether a specific sentinel error appears somewhere along the "error chain".
64+
It does that by secretly calling `Unwrap` repeatedly until the error in question was found or the chain ended.
65+
66+
```go
67+
originalError := errors.New("unexpected negative number")
68+
err := fmt.Errorf("parsing age: %w", originalError)
69+
errors.Is(err, originalError)
70+
// => true
71+
```
72+
73+
Checking for the original error with `errors.Is` would not work with the regular `%v` formatting verb.
74+
75+
```go
76+
originalError := errors.New("unexpected negative number")
77+
err := fmt.Errorf("parsing age: %v", originalError)
78+
errors.Is(err, originalError)
79+
// => false
80+
```
81+
82+
As a result, it is good practice to use `%v` by default and only use `%w` if you explicitly want to allow your consumer to access an underlying error ([Google Go Styleguide][google-go-styleguide]).
83+
84+
If you find ourself in a situation where you want to check for a sentinel error but explicitly don't want to unwrap, you can use `==` instead of `errors.Is`.
85+
86+
```go
87+
var originalError = errors.New("unexpected negative number")
88+
func someFunc() error {
89+
return originalError
90+
}
91+
92+
err := someFunc()
93+
err == originalError
94+
// => true
95+
```
96+
97+
In contrast, `==` would not identify the error if it was wrapped.
98+
99+
```go
100+
var originalError = errors.New("unexpected negative number")
101+
func someFunc() error {
102+
return fmt.Errorf("parsing age: %v", originalError)
103+
}
104+
105+
err := someFunc()
106+
err == originalError
107+
// => false
108+
```
109+
110+
It is fine to work with `errors.Is` by default, but you should always be aware that this means the whole error chain will be searched.
111+
112+
## Checking for (custom) error types
113+
114+
There is an equivalent to `errors.Is` that allows to check for and retrieve an error of a specific type from the error chain.
115+
The function for that is `errors.As` and just like `errors.Is` it will search for the given error type along the whole chain.
116+
`errors.As` does only then extract the error it found that matches the type so you can further work with it, e.g. retrieve specific fields.
117+
118+
```go
119+
type MyCustomError struct {
120+
Message string
121+
Details string
122+
}
123+
124+
func (e *MyCustomError) Error() string {
125+
return fmt.Sprintf("%s, details: %s", e.Message, e.Details)
126+
}
127+
128+
func someFunc() error {
129+
originalError := &MyCustomError{
130+
Message: "some message",
131+
Details: "some details",
132+
}
133+
134+
return fmt.Errorf("doing something: %w", originalError)
135+
}
136+
137+
err := someFunc()
138+
var customError *MyCustomError
139+
errors.As(err, &customError)
140+
// => true
141+
142+
// customError now contains the error that was found in the error chain.
143+
customError.Details
144+
// => "some details"
145+
```
146+
147+
~~~~exercism/caution
148+
Be careful with the syntax regarding the pointers above.
149+
The code will only work and compile correctly if `customError` has the exact type that implements the error interface.
150+
In our case, that is `*MyCustomError` (a [pointer][concept-pointers] to `MyCustomError`), not `MyCustomError` itself.
151+
152+
On top of that, the second argument needs to be a pointer to the error variable.
153+
Since our error is already a pointer, what we are passing to `errors.As` is a pointer to a pointer (to MyCustomError).
154+
Only with this set up correctly, Go can then fill the variable with the error it found.
155+
~~~~
156+
157+
As before, `errors.As` would not have found the error type if `%v` would have been used when calling `Errorf`.
158+
159+
If you don't want to unwrap for some reason, type assertion can be used instead (equivalent to `==` above).
160+
161+
```go
162+
// MyCustomError defined as above.
163+
164+
func someFunc() error {
165+
return &MyCustomError{
166+
Message: "some message",
167+
Details: "some details",
168+
}
169+
}
170+
171+
err := someFunc()
172+
customError, ok := err.(*CustomError)
173+
// "ok" is now true
174+
customError.Details
175+
// => "some details"
176+
```
177+
178+
Type assertion will not be able to identify the error type if the error would have been wrapped.
179+
180+
## Allowing errors of custom types to be unwrapped
181+
182+
Sometimes just wrapping an error with some additional text is not enough.
183+
You can create a custom error type instead that holds the original error and the additional structured data that you want to add.
184+
If you want to allow unwrapping for your error type, the only thing you have to do is to manually add an `Unwrap() error` method so the `Unwrap` interface is satisfied.
185+
186+
```go
187+
type SpecialError struct {
188+
originalError error
189+
metadata string
190+
}
191+
192+
func (e *SpecialError) Error() string {
193+
// The usual serialization code goes here.
194+
}
195+
196+
func (e *SpecialError) Unwrap() error {
197+
return e.originalError
198+
}
199+
```
200+
201+
## Combining multiple errors
202+
203+
In Go 1.20, a new `Join` function was added to the built-in `errors` package.
204+
`Join` allows it to combine multiple errors together in a way that still allows unwrapping, i.e. checking for a specific error or error type along the chain.
205+
With that, the error chain can actually be an error tree in reality.
206+
More information can be found in the [release notes][release-notes] and the [documentation][doc-join].
207+
208+
[concept-errors]: /tracks/go/concepts/errors
209+
[concept-pointers]: /tracks/go/concepts/pointers
210+
[google-go-styleguide]: https://google.github.io/styleguide/go/best-practices#adding-information-to-errors
211+
[release-notes]: https://tip.golang.org/doc/go1.20#errors
212+
[doc-join]: https://pkg.go.dev/errors#Join
+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Introduction
2+
3+
## Adding context to errors
4+
5+
We explored basic error handling in Go in the [errors concept][concept-errors].
6+
As you learned there, by default errors do not carry around stack traces.
7+
That makes it crucial to ensure the error itself contains enough information to identify the problem.
8+
9+
If we wanted to add information to an existing error with the tools we already know, we could write something like this to create a new error with the combined text of the original error and the additional information:
10+
11+
```go
12+
err := errors.New(fmt.Sprintf("parsing age for user %d: %v", userID, originalError))
13+
```
14+
15+
Luckily, the `fmt` package from the standard library contains a short-hand version for this in form of the `Errorf` function.
16+
That allows you, for example, to easily add information about the context in which the error occurred.
17+
18+
```go
19+
originalError := errors.New("unexpected negative number")
20+
userID := 123
21+
err := fmt.Errorf("parsing age for user %d: %v", userID, originalError)
22+
err.Error()
23+
// => "parsing age for user 123: unexpected negative number"
24+
```
25+
26+
Often this way of adding information for an error is good enough but there are cases where you want to allow the consumer of your error to check for or retrieve the original underlying error.
27+
Adding context in way that allows this is called "wrapping an error" in Go.
28+
29+
## Wrapping errors and checking for sentinel errors
30+
31+
Error wrapping can be achieved with a very minor change to the code we saw above.
32+
To wrap an error, you need to use the formatting verb `%w` instead of `%v`.
33+
Behind the scenes, this will make sure that the resulting error implements an `Unwrap` method which returns the original error.
34+
Because of that, then `errors.Is` can be used to check whether a specific sentinel error appears somewhere along the "error chain".
35+
It does that by secretly calling `Unwrap` repeatedly until the error in question was found or the chain ended.
36+
37+
```go
38+
originalError := errors.New("unexpected negative number")
39+
err := fmt.Errorf("parsing age: %w", originalError)
40+
errors.Is(err, originalError)
41+
// => true
42+
```
43+
44+
It is good practice to use `%v` by default and only use `%w` if you explicitly want to allow your consumer to access an underlying error ([Google Go Styleguide][google-go-styleguide]).
45+
46+
If you find ourself in a situation where you want to check for a sentinel error but explicitly don't want to unwrap, you can use `==` instead of `errors.Is`.
47+
48+
```go
49+
var originalError = errors.New("unexpected negative number")
50+
func someFunc() error {
51+
return originalError
52+
}
53+
54+
err := someFunc()
55+
err == originalError
56+
// => true
57+
```
58+
59+
It is fine to work with `errors.Is` by default, but you should always be aware that this means the whole error chain will be searched.
60+
61+
## Checking for (custom) error types
62+
63+
There is an equivalent to `errors.Is` that allows to check for and retrieve an error of a specific type from the error chain.
64+
The function for that is `errors.As` and just like `errors.Is` it will search for the given error type along the whole chain.
65+
`errors.As` does only then extract the error it found that matches the type so you can further work with it, e.g. retrieve specific fields.
66+
67+
```go
68+
type MyCustomError struct {
69+
Message string
70+
Details string
71+
}
72+
73+
func (e *MyCustomError) Error() string {
74+
return fmt.Sprintf("%s, details: %s", e.Message, e.Details)
75+
}
76+
77+
func someFunc() error {
78+
originalError := &MyCustomError{
79+
Message: "some message",
80+
Details: "some details",
81+
}
82+
83+
return fmt.Errorf("doing something: %w", originalError)
84+
}
85+
86+
err := someFunc()
87+
var customError *MyCustomError
88+
errors.As(err, &customError)
89+
// => true
90+
91+
// customError now contains the error that was found in the error chain.
92+
customError.Details
93+
// => "some details"
94+
```
95+
96+
~~~~exercism/caution
97+
Be careful with the syntax regarding the pointers above.
98+
The code will only work and compile correctly if `customError` has the exact type that implements the error interface.
99+
In our case, that is `*MyCustomError` (a [pointer][concept-pointers] to `MyCustomError`), not `MyCustomError` itself.
100+
101+
On top of that, the second argument needs to be a pointer to the error variable.
102+
Since our error is already a pointer, what we are passing to `errors.As` is a pointer to a pointer (to MyCustomError).
103+
Only with this set up correctly, Go can then fill the variable with the error it found.
104+
~~~~
105+
106+
As before, `errors.As` would not have found the error type if `%v` would have been used when calling `Errorf`.
107+
108+
If you don't want to unwrap for some reason, type assertion can be used instead (equivalent to `==` above).
109+
110+
```go
111+
// MyCustomError defined as above.
112+
113+
func someFunc() error {
114+
return &MyCustomError{
115+
Message: "some message",
116+
Details: "some details",
117+
}
118+
}
119+
120+
err := someFunc()
121+
customError, ok := err.(*CustomError)
122+
// "ok" is now true
123+
customError.Details
124+
// => "some details"
125+
```
126+
127+
Type assertion will not be able to identify the error type if the error would have been wrapped.
128+
129+
## Allowing errors of custom types to be unwrapped
130+
131+
Sometimes just wrapping an error with some additional text is not enough.
132+
You can create a custom error type instead that holds the original error and the additional structured data that you want to add.
133+
If you want to allow unwrapping for your error type, the only thing you have to do is to manually add an `Unwrap() error` method so the `Unwrap` interface is satisfied.
134+
135+
```go
136+
type SpecialError struct {
137+
originalError error
138+
metadata string
139+
}
140+
141+
func (e *SpecialError) Error() string {
142+
// The usual serialization code goes here.
143+
}
144+
145+
func (e *SpecialError) Unwrap() error {
146+
return e.originalError
147+
}
148+
```
149+
150+
[concept-errors]: /tracks/go/concepts/errors
151+
[concept-pointers]: /tracks/go/concepts/pointers
152+
[google-go-styleguide]: https://google.github.io/styleguide/go/best-practices#adding-information-to-errors
153+
[release-notes]: https://tip.golang.org/doc/go1.20#errors
154+
[doc-join]: https://pkg.go.dev/errors#Join

concepts/error-wrapping/links.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"url": "https://go.dev/blog/go1.13-errors",
4+
"description": "Go Blog: Working with Errors in Go 1.13"
5+
},
6+
{
7+
"url": "https://www.digitalocean.com/community/tutorials/how-to-add-extra-information-to-errors-in-go",
8+
"description": "Digital Ocean: How to Add Extra Information to Errors in Go"
9+
}
10+
]

config.json

+5
Original file line numberDiff line numberDiff line change
@@ -2204,6 +2204,11 @@
22042204
"name": "Variadic Functions",
22052205
"slug": "variadic-functions",
22062206
"uuid": "7c9ad69f-b28a-4366-affc-870fc48e51eb"
2207+
},
2208+
{
2209+
"name": "Error Wrapping",
2210+
"slug": "error-wrapping",
2211+
"uuid": "f22b0e85-4d86-4ecf-9415-ea27c93ebcd6"
22072212
}
22082213
],
22092214
"key_features": [

0 commit comments

Comments
 (0)