A super tiny package for error encapsulation and composition in idiomatic Go. A better and lighter alternative to pkg/errors
.
(See below for comparison to pkg/errors
)
Note: This package was previously called
goerror
. Now renamed toerrors
to be a drop-in replacement for stderrors
package.
Documentation: Read the source, Luke
- it's tiny.
go get -u github.com/prasannavl/go-errors
httperror
- ProvidesHttpError
interface andHttpErr
typeerrutils
- Provides a set of helpers to ease iteration, and message aggregation
Provides 3 interfaces
GoError
CodedError
ErrorGroup
Provides 3 concrete types
GoErr
CodedErr
ErrGroup
GoError
provides a straightforward way to wrap the error
interface as Cause
. The utils
subpackage provides a way to iterate over chained errors and to collect error messages easily.
CodedError
adds just one item to Error
- Code
. It's useful for building any errors with an int
code. The httperror
subpackage, for example simply builds over this with an added code validation.
ErrGroup
provides a clean way to aggregate errors as []error
. But unlike other libraries out there - doesn't provides any way to add, combine, or remove errors. It prefers to sticks to Go's idiomatic way of keeping things minimalistic. You can use a Go's own make
, append
and other slice function to do the same. Once done, you simply use GroupFrom
to add your slice to create a group. It's all just slices. The only thing ErrGroup
does is provides you an error
interface that by default prints a nice message, and a way to retrive all the original errors.
func TestErrors() {
// New errors
err := errors.New("some error")
// Error with codes
codedErr := errors.NewCoded(42, "compute overwhelmed. crashing")
// HttpErrors are essentially, coded errors,
// with http code validation
httpErr := httperror.New(400, "naughty you! that's not valid", true)
// Wrap existing existing errors
wrapped1 := errors.From(errors.New("some other error"))
wrapped2 := errors.From(wrapped1)
// With additional message
wrapper3 := errors.NewWithCause("wrapped as different msg", wrapped2)
// Intuitively messages:
fmt.Println(wrapped1)
// Output
// some other error
// Same output
fmt.Println(wrapped2)
// Output
// some other error
fmt.Println(wrapped3)
// Output
// wrapped as different msg
// You can get the real err by accessing `Cause`. Or more intuitively,
// Collect from messages
msgs := errutils.CollectMsg(wrapped3, nil)
// msg has [ 'wrapped as different msg', 'some other error' ]
// Error group
errs := []error { err, codedErr, httpErr, wrapped1, wrapped2}
errGroup := errors.GroupFrom(errs)
// Let's make things interesting
fmt.Println(errGroup)
// Output
// multiple errors:
// ...
// prints all of the given errors
// ...
// Or take control yourself
allErrs = errGroup.Errors()
msgs = errutils.CollectMsg(allErrs, nil)
}
A real example in middleware using prasannavl/mchain
func RequestIDMustInitHandler(next mchain.Handler) mchain.Handler {
f := func(w http.ResponseWriter, r *http.Request) error {
c := FromRequest(r)
if _, ok := r.Header[RequestIDHeaderKey]; ok {
msg := fmt.Sprintf("error: illegal header (%s)", RequestIDHeaderKey)
return httperror.New(400, msg, true)
}
var uid uuid.UUID
mustNewUUID(&uid)
c.RequestID = uid
return next.ServeHTTP(w, r)
}
return mchain.HandlerFunc(f)
}
Some of the features provided by this package are very similar to what the package pkg/errors
offers. So, why do another error package?
-
pkg/errors
has a very nice and simple way to chain errors withCause
. However, other than that I tend to disagree with the pattern the pkg/errors package tends to build on. In a language with exceptions, stack traces are critical - and so even though it may be expensive to take them - we try to get it where-ever we can. But Go takes a drastically different approach towards error handling. It's very simple. You just pass a simple structure around. And as such, is very lightweight. And it's very clean because, in order to identify where an error originated from, you can create errors that are unique with codes (which is the one of the purpose of error codes in the first place, beyond internal handling of error such as comparisons). Butpkg/errors
destroys this "lightness" by callingruntime.Callers
everytime you create an error. I find taking stack traces along with every error mindlessly to be wasteful in a language without exceptions, and not idiomatic go.This package solves the problems that
pkg/errors
try to solve, and a lot more - by ErrorGroups, CodedError and utils, all while still retaining more simplicity thanpkg/errors
.
Other than this, goerror
also provides a more complete error encapsulation solution:
- It provides
CodedError
pattern by out of the box - which is what a lot of usage requires rather than stack traces. - Defines a neat way to group errors with
GroupError
. - It also provides
httperror
that's built overCodedError
, out of the box but as separate package.
As mentioned, stack-traces are absolutely essential when you work with language with execptions. Because they can throw from anywhere, and you can be catch
ing it elsewhere - you're bound to loose track of where you are very quickly. However the approach Go takes is simple. You check your errors at every surface of an API. So, simple error type with kind can work very nicely. And that doesn't mean you can't or shouldn't take stack traces. You can always just wrap them in higher order functions. I've found it to be a better practice to take traces at component or contextual boundaries, when needed.
Simple composable errors with codes, should be the basic building block of error handling - while a package like pkg/errors
that's intended to be a fundamental building block, unfortunately does caller stacks, is structured far more complex internally, and doesn't really help much with composition
Then take them!
It's just not intended to go into the building block. They should be above it. So you have exactly that choice. There are times when stack traces are critical, and there are times when they are just a waste of system resources - but the truth is, error handling helpers like these can never know that regardless of any number of debates or discussions - and so this package does the absolute minimum to act as a building block, yet provides nice composition. Rest can be built over it.
Don't let the number of types fool you. Conceptually, it's all just in one file errors.go
, which is less than 50 lines long. All it really does is just wrap things up neatly in a type, and allows you to compose them. If you're a reasonably proficient in Go, you should be able to read and understand the whole code of the library in just under 5 minutes - I encourage you to - you should know exactly what happens you instantiate an error type.
Yup. It is!
HttpError
provides one additional method End
that's useful to signify any middleware chain to stop processing.
Combining GoError
and ErrorGroup
should be sufficient to handle most complex error wrapping, merging scenarios in Go without the use of other packages that add too many whistles which are, in my opinion completely unnecessary - and frankly not idiomatic Go.
This project is licensed under either of the following, at your choice:
- Apache License, Version 2.0, (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- GPL 3.0 license (LICENSE-GPL or https://opensource.org/licenses/GPL-3.0)
Contribution to the LiquidState project is organized under the terms of the Contributor Covenant, and as such the maintainer @prasannavl promises to intervene to uphold that code of conduct.