Skip to content

Commit 5c0d477

Browse files
committed
Initial open source release of go-errors.
1 parent e9857f7 commit 5c0d477

File tree

11 files changed

+1299
-0
lines changed

11 files changed

+1299
-0
lines changed

.editorconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
; indicate this is the root of the project
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
end_of_line = LF
7+
insert_final_newline = true
8+
trim_trailing_whitespace = true
9+
indent_style = space
10+
indent_size = 4
11+
12+
[Makefile]
13+
indent_style = tab
14+
15+
[*.go]
16+
indent_style = tab

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Test binary, build with `go test -c`
2+
*.test
3+
4+
# Output of the go coverage tool, specifically when used with LiteIDE
5+
*.out
6+
*.svg

NOTES.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Notes
2+
3+
## Output
4+
5+
Errors are only used in a few places:
6+
7+
* Programmatically, i.e. checking the kind of an error. No output.
8+
* In logs, i.e. when they have bubbled up through the application.
9+
* Output to the user, i.e. how a web application might respond, or perhaps more likely with Go, in
10+
an API response.
11+
12+
Programmatically, we don't really have any issues. We are only looking for an error kind. This is a
13+
simple comparison through the stack.
14+
15+
In logs, we are by far more likely to use a message, a stack trace, and also log the fields of all
16+
errors as structured log fields. This part is fine really, we'll have all of the information we need
17+
and it should work fine in Kibana too. The message is a little bit pointless, but might make it
18+
easier to filter for certain top-level errors. By providing fields as a generic map, it also means
19+
we have a consistent format for submitting structured errors as logs, no matter what information is
20+
in the error.
21+
22+
Finally, errors that are output to the user. So, most of the time we can probably get away with some
23+
kind of generic "an internal error occurred, contact someone @ wherever" kind of messages. What
24+
about those times where we might want a more structured error format? Really we're talking about two
25+
distinct concepts; errors, and error responses. Given that we have fields on errors, we should be
26+
able to use that to build error responses if we need to. We can base our expectations surrounding
27+
the fields that should be present on an error on the kind of error that is encountered. In other
28+
words, we could make an error kind like `deals: submission validation`, and along with that kind of
29+
error, always make sure we set a certain set of fields. For structs that need to be attached as
30+
fields we can still make use of the fields.
31+
32+
This does introduce another issue; what if we don't want to log all fields? Well then in that case,
33+
they could always be removed. If we can expect certain fields to be there, we can expect them and
34+
then remove them too. Simple really.

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# go-errors
2+
3+
This package aims to provide flexible general-purpose error handling functionality, with the
4+
following specific features in mind:
5+
6+
* **Error wrapping**: Allowing an error's cause to be preserved along with additional information.
7+
* **Stack tracing**: Allowing the path taken to return an error to be easily identified.
8+
* **Structured fields**: Allowing errors to be logged with additional contextual information.
9+
10+
This library has built upon the mistakes we've made and lessons we've learnt with regards to error
11+
handling at Icelolly whilst working on our internal APIs. This library was inspired by approaches
12+
found elsewhere in the community, most notably the approach found in [Upspin][1].
13+
14+
## Example
15+
16+
An example of usage can be found in the `example/` directory. It showcases creating errors, wrapping
17+
them, handling different kinds of errors, and dealing with things like logging.
18+
19+
## See More
20+
21+
* https://github.com/upspin/upspin/tree/master/errors
22+
* https://middlemost.com/failure-is-your-domain/
23+
24+
25+
[1]: https://github.com/upspin/upspin/blob/master/errors/errors.go#L23

errors.go

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
package errors
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"runtime"
8+
"sort"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
// Kind is simply a string, but it allows New to function the way it does, and limits what can be
14+
// passed as the kind of an error to things defined as actual error kinds.
15+
type Kind string
16+
17+
// Error is a general-purpose error type, providing much more contextual information and utility
18+
// when compared to the built-in error interface.
19+
type Error struct {
20+
// Kind can be used as a sort of pseudo-type that check on. It's a useful mechanism for avoiding
21+
// "sentinel" errors, or for checking an error's type. Kind is defined as a string so that error
22+
// kinds can be defined in other packages.
23+
Kind Kind
24+
25+
// Message is a human-readable, user-friendly string. Unlike caller, Message is really intended
26+
// to be user-facing, i.e. safe to send to the front-end.
27+
Message string
28+
29+
// Cause is the previous error. The error that triggered this error. If it is nil, then the root
30+
// cause is this Error instance. If Cause is not nil, but also not of type Error, then the root
31+
// cause is the error in Cause.
32+
Cause error
33+
34+
// Fields is a general-purpose map for storing key/value information. Useful for providing
35+
// additional structured information in logs.
36+
Fields map[string]interface{}
37+
38+
// caller is the function that was called when this error occurred. Useful for identifying where
39+
// an error occurred, or providing information to developers (i.e. this should not be revealed
40+
// or used in responses / sent to the front-end). This may be something as simple as the method
41+
// name being called, or perhaps include more information to do with parameters.
42+
caller string
43+
44+
// Stack location information.
45+
file string
46+
line int
47+
}
48+
49+
// Error satisfies the standard library's error interface. It returns a message that should be
50+
// useful as part of logs, as that's where this method will likely be used most, including the
51+
// caller, and the message, for the whole stack.
52+
func (e *Error) Error() string {
53+
return e.format(false)
54+
}
55+
56+
// Format allows this error to be formatted differently, depending on the needs of the developer.
57+
// The different formatting options made available are:
58+
//
59+
// %v: Standard formatting: shows callers, and shows messages, for the whole stack.
60+
// %+v: Verbose formatting: shows callers, and shows messages, for the whole stack, with file and
61+
// line, information, across multiple lines.
62+
func (e *Error) Format(s fmt.State, c rune) {
63+
if c == 'v' && s.Flag('+') {
64+
io.WriteString(s, e.format(true))
65+
return
66+
}
67+
68+
io.WriteString(s, e.format(false))
69+
}
70+
71+
// WithFields appends a set of key/value pairs to the error's field list.
72+
func (e *Error) WithFields(kvs ...interface{}) *Error {
73+
kvc := len(kvs)
74+
75+
if kvc%2 != 0 {
76+
Fatal(New(fmt.Sprintf(
77+
"errors: invalid argument count for WithFields, expected even number of fields, got %d",
78+
kvc,
79+
)))
80+
}
81+
82+
if e.Fields == nil {
83+
e.Fields = make(map[string]interface{})
84+
}
85+
86+
for i := 0; i < kvc; i = i + 2 {
87+
key, ok := kvs[i].(string)
88+
if !ok {
89+
Fatal(New(fmt.Sprintf("errors: invalid type for key passed to WithFields at index %d", i)))
90+
}
91+
92+
e.Fields[key] = kvs[i+1]
93+
}
94+
95+
return e
96+
}
97+
98+
// WithField appends a key/value pair to the error's field list.
99+
func (e *Error) WithField(fieldKey string, fieldValue interface{}) *Error {
100+
if e.Fields == nil {
101+
e.Fields = make(map[string]interface{})
102+
}
103+
104+
e.Fields[fieldKey] = fieldValue
105+
106+
return e
107+
}
108+
109+
// format returns this error, and all previous errors, as a string. The result can be represented as
110+
// a multi-line stack-trace by setting `asStack` to true.
111+
func (e *Error) format(asStack bool) string {
112+
// Buffer is shared between recursive calls to avoid some unnecessary re-allocations.
113+
buf := bytes.Buffer{}
114+
115+
e.formatAccumulator(&buf, asStack, false)
116+
117+
return buf.String()
118+
}
119+
120+
// formatAccumulator is a recursive error formatting function.
121+
func (e *Error) formatAccumulator(buf *bytes.Buffer, asStack, isCause bool) {
122+
if asStack && !isCause {
123+
buf.WriteString("Error")
124+
}
125+
126+
if e.caller != "" {
127+
pad(buf, ": ")
128+
buf.WriteString("[")
129+
buf.WriteString(e.caller)
130+
buf.WriteString("]")
131+
}
132+
133+
if e.Message != "" {
134+
pad(buf, ": ")
135+
buf.WriteString(e.Message)
136+
}
137+
138+
if e.Kind != "" {
139+
pad(buf, " ")
140+
buf.WriteString("(")
141+
buf.WriteString(string(e.Kind))
142+
buf.WriteString(")")
143+
}
144+
145+
if asStack {
146+
buf.WriteString("\n")
147+
buf.WriteString(" ")
148+
buf.WriteString("File: \"")
149+
buf.WriteString(e.file)
150+
buf.WriteString("\", line ")
151+
buf.WriteString(strconv.Itoa(e.line))
152+
buf.WriteString("\n")
153+
154+
if len(e.Fields) > 0 {
155+
buf.WriteString(" ")
156+
buf.WriteString("With fields:\n")
157+
158+
fieldKeys := make([]string, 0, len(e.Fields))
159+
for k := range e.Fields {
160+
fieldKeys = append(fieldKeys, k)
161+
}
162+
163+
sort.Strings(fieldKeys)
164+
165+
for _, k := range fieldKeys {
166+
buf.WriteString(" ")
167+
buf.WriteString("- \"")
168+
buf.WriteString(k)
169+
buf.WriteString("\": ")
170+
buf.WriteString(fmt.Sprintf("%v", e.Fields[k]))
171+
buf.WriteString("\n")
172+
}
173+
}
174+
}
175+
176+
if e.Cause != nil {
177+
if !asStack {
178+
} else {
179+
buf.WriteString("Caused by")
180+
}
181+
182+
switch cause := e.Cause.(type) {
183+
case *Error:
184+
cause.formatAccumulator(buf, asStack, true)
185+
case error:
186+
pad(buf, ": ")
187+
buf.WriteString(cause.Error())
188+
}
189+
}
190+
}
191+
192+
// pad takes a buffer and if it's not empty, writes the given padding string to it.
193+
func pad(buf *bytes.Buffer, pad string) {
194+
if buf.Len() > 0 {
195+
buf.WriteString(pad)
196+
}
197+
}
198+
199+
// New returns a new error. New accepts a variadic list of arguments, but at least one argument must
200+
// be specified, otherwise New will panic. New will also panic if an unexpected type is given to it.
201+
// Each field that can be set on an *Error is of a different type, meaning we can switch on the type
202+
// of each argument, and still know which field to set on the error, leaving New as a very flexible
203+
// function that is also not overly verbose to call.
204+
//
205+
// Example usage:
206+
//
207+
// // Create the initial error, maybe this would be returned from some function.
208+
// err := errors.New(ErrKindTimeout, "client: HTTP request timed out")
209+
// // Wrap an existing error. It can be a regular error too. Also, set a field.
210+
// err = errors.New(err, "accom: fetch failed", errors.WithField("tti_code", ttiCode))
211+
//
212+
// As you can see, this usage is flexible, and includes the ability to construct pretty much any
213+
// kind of error your application should need.
214+
func New(args ...interface{}) *Error {
215+
err := newError(args...)
216+
217+
updateLocation(err)
218+
updateCaller(err)
219+
220+
return err
221+
}
222+
223+
// newError creates a new *Error instance, returning it as an *Error, so that we can operate on it
224+
// internally without having to cast back to *Error.
225+
func newError(args ...interface{}) *Error {
226+
if len(args) == 0 {
227+
panic("errors: call to errors.New with no arguments")
228+
}
229+
230+
err := &Error{}
231+
for _, arg := range args {
232+
switch v := arg.(type) {
233+
case Kind:
234+
err.Kind = v
235+
case string:
236+
err.Message = v
237+
case *Error:
238+
// Can't dereference a nil pointer, so bail early. This is a developer error.
239+
if v == nil {
240+
panic("errors: attempted to wrap nil *Error")
241+
}
242+
243+
// Make a shallow copy of the value, so that we don't change the original error.
244+
cv := *v
245+
err.Cause = &cv
246+
case error:
247+
err.Cause = v
248+
case map[string]interface{}:
249+
err.Fields = v
250+
default:
251+
panic(fmt.Sprintf("errors: bad call to errors.New: unknown type %T, value %v", arg, arg))
252+
}
253+
}
254+
255+
return err
256+
}
257+
258+
// Wrap constructs an error the same way that New does, the only difference being that if the given
259+
// cause is nil, this function will return nil. This makes it quite handy in return lines at the end
260+
// of functions. Wrap conveys it's meaning a little more than New does when you are wrapping other
261+
// errors.
262+
func Wrap(cause error, args ...interface{}) *Error {
263+
if cause == nil {
264+
return nil
265+
}
266+
267+
// Add the cause to the end of args so that it is definitely set as the cause.
268+
args = append(args, cause)
269+
err := newError(args...)
270+
271+
// We have to set these again, as they'll be at the wrong depth now.
272+
updateLocation(err)
273+
updateCaller(err)
274+
275+
return err
276+
}
277+
278+
// updateCaller takes an error and sets the calling function information on it. Safe to use in error
279+
// constructors, but no deeper.
280+
func updateCaller(err *Error) {
281+
fpcs := make([]uintptr, 1)
282+
ptr := runtime.Callers(3, fpcs)
283+
if ptr == 0 {
284+
return
285+
}
286+
287+
fun := runtime.FuncForPC(fpcs[0] - 1)
288+
if fun != nil {
289+
funcName := strings.Split(fun.Name(), "/")
290+
err.caller = funcName[len(funcName)-1]
291+
}
292+
}
293+
294+
// updateLocation takes an error and sets file and line information on it. Safe to use in error
295+
// constructors, but no deeper.
296+
func updateLocation(err *Error) {
297+
_, file, line, _ := runtime.Caller(2)
298+
err.file = file
299+
err.line = line
300+
}

0 commit comments

Comments
 (0)