Builds on Go 1.13 errors by adding HTTP statuses and GRPC codes to them.
go get -u github.com/stackus/errors
Go 1.20 or later is required to use this package.
This library allows the use and helps facilitate the embedding of a type code, HTTP status, and GRPC code into errors that can then be shared between services.
Type codes are strings that are returned by any error that implements errors.TypeCoder
.
type TypeCoder interface {
error
TypeCode() string
}
HTTP statuses are integer values that have defined in the net/http
package and are returned by any error that
implements errors.HTTPCoder
.
type HTTPCoder interface {
error
HTTPCode() int
}
GRPC codes are codes.Code
are int64 values defined in the google.golang.org/grpc/codes
package and are returned by
any error that implements errors.GRPCCoder
.
type GRPCCoder interface {
error
GRPCCode() codes.Code
}
The package also comes with many defined errors that are named in a way to reflect the GRPC code or HTTP status they
represent. The list of embeddable errors.Error
types can be
found here.
The errors.Wrap(error, string) error
function is used to wrap errors combining messages in most cases. However, when
the function is used with an error that has implemented errors.TypeCoder
the message is not altered, and the error is
embedded instead.
// Wrapping normal errors appends the error message
err := errors.Wrap(fmt.Errorf("sql error"), "error message")
fmt.Println(err) // Outputs: "error message: sql error"
// Wrapping errors.TypeCoder errors embeds the type
err := errors.Wrap(errors.ErrNotFound, "error message")
fmt.Println(err) // Outputs: "error message"
Wrapping multiple times will add additional prefixes to the error message.
// Wrapping multiple times
err := errors.Wrap(errors.ErrNotFound, "error message")
err = errors.Wrap(err, "prefix")
err = errors.Wrap(err, "another")
fmt.Println(err) // Outputs: "another: prefix: error message"
It is possible to use the package errors to wrap existing errors to add or override Type, HTTP code, or GRPC status codes.
// Err will use the wrapped error .Error() output as the message
err := errors.ErrBadRequest.Err(fmt.Errorf("some error"))
// Msg and Msgf returns the Error with just the custom message applied
err = errors.ErrBadRequest.Msgf("%d total reasons", 7)
// Wrap and Wrapf will accept messages and simple wrap the error
err = errors.ErrUnauthorized.Wrap(err, "some message")
Both errors can be checked for using the Is()
and As()
methods when you wrap errors with the package errors this way.
The Go 1.13 errors.As(error, interface{}) bool
function from the standard errors
package can be used to turn an
error into any of the three "Coder" interfaces documented above.
err := errors.Wrap(errors.NotFound, "error message")
var coder errors.TypeCoder
if errors.As(err, &coder) {
fmt.Println(coder.TypeCode()) // Outputs: "NOT_FOUND"
}
The functions
Is()
,As()
, andUnwrap()
from the standarderrors
package have all been made available in this package as proxies for convenience.
The functions errors.TypeCode(error) string
, errors.HTTPCode(error) int
, and errors.GRPCCode(error) codes.Code
can
be used to fetch specific code. They're more convenient to use than the interfaces directly. The catch is they have
defined rules for the values they return.
The function errors.Join(errs ...error) error
, made available in Go 1.20 has been added to this package as an additional convenience.
If the error implements or has wrapped an error that implements errors.TypeCoder
it will return the code from that
error. If no error is found to support the interface then the string "UNKNOWN"
is returned. Nil errors result in a
blank string being returned.
fmt.Println(errors.TypeCode(errors.ErrNotFound)) // Outputs: "NOT_FOUND"
fmt.Println(errors.TypeCode(fmt.Errorf("an error"))) // Outputs: "UNKNOWN"
fmt.Println(errors.TypeCode(nil)) // Outputs: ""
If the error implements or has wrapped an error that implements errors.HTTPCoder
it will return the status from that
error. If no error is found to support the interface then http.StatusNotExtended
is returned. Nil errors result
in http.StatusOK
being returned.
fmt.Println(errors.HTTPCode(errors.ErrNotFound)) // Outputs: 404
fmt.Println(errors.HTTPCode(fmt.Errorf("an error"))) // Outputs: 510
fmt.Println(errors.HTTPCode(nil)) // Outputs: 200
If the error implements or has wrapped an error that implements errors.GRPCCoder
it will return the code from that
error. If no error is found to support the interface then codes.Unknown
is returned. Nil errors result in codes.OK
being returned.
fmt.Println(errors.GRPCCode(errors.ErrNotFound)) // Outputs: 5
fmt.Println(errors.GRPCCode(fmt.Errorf("an error"))) // Outputs: 2
fmt.Println(errors.GRPCCode(nil)) // Outputs: 0
Part of the reason you'd want to use a library that adds code to your errors is because you want to better identify the
problems in your application. By marking un-coded errors as "Unknown" errors they'll stand out from any errors you've
marked as codes.Internal
for example.
The functions SendGRPCError(error) error
and ReceiveGRPCError(error) error
provide a way to convert
a status.Status
and its error into an error that provides codes and vice versa. You can use these in your server and
client handlers directly, or they can be used with GRPC interceptors.
Server Interceptor Example:
// Unary only example
func serverErrorUnaryInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
return resp, errors.SendGRPCError(err)
}
}
server := grpc.NewServer(grpc.ChainUnaryInterceptor(serverErrorUnaryInterceptor()), ...others)
Client Interceptor Example:
// Unary only example
func clientErrorUnaryInterceptor() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return errors.ReceiveGRPCError(invoker(ctx, method, req, reply, cc, opts...))
}
}
cc, err := grpc.Dial(uri, grpc.WithChainUnaryInterceptor(clientErrorUnaryInterceptor()), ...others)
Servers and clients may not always use a shared library when exchanging errors. In fact there isn't any requirement that the server and client both use this library to exchange errors.
When comparing received errors with errors.Is(error, error) bool
the checks are a little more loose. A received error
is considered to be the same if ANY of the codes are a match. This differs from a strict equality check for the
server before the error was sent.
The "Code" functions and the "Coder" interfaces continue to work the same on a client as they did on the server that sent the error.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
MIT