Skip to content

Instantly share code, notes, and snippets.

@dexterp
Last active May 3, 2024 18:26
Show Gist options
  • Save dexterp/1edee19aa54d87f33d3cc727fde86196 to your computer and use it in GitHub Desktop.
Save dexterp/1edee19aa54d87f33d3cc727fde86196 to your computer and use it in GitHub Desktop.
GO Error wrapping in Go
// Code is error kind
package errs
import (
"errors"
"fmt"
"internal/resrc/call"
)
// Op the operation that caused the error
type Op string
// Code the error kind
type Code int
const (
None Code = iota
Unexpected
// Used when a type switch matches a unknown or invalid type
InvalidType
// App errors
App Code = iota + 2000
AppNoHost
AppNoIP
// Internal errors
Int Code = iota + 5000
IntSQLDelete
IntSQLInsert
IntSQLRowsAffected
IntSQLSelect
)
// Error
type Error struct {
Op Op // operation (where in your code is this error )
Kind Code // what kind of error is this (user, unexpected)
Err error // wrapped error
}
// Error returns the error string of the outer most leaf node.
func (e Error) Error() string {
return fmt.Sprint(Unwrap(e.Err))
}
// E is the main error function to denote "leaf" errors and "branch" errors.
// A leaf error is one that includes a code and an error value or string.
// Leaf example:
//
// // with a string as the error
// errs.E(errs.UserSyntax, "the error message")
//
// // with a an err as the error
// f, err := os.Open("some_file.txt")
// if err != nil {
// return errs.E(errs.Unexpected, err)
// }
//
// A branch error is one that passes the error up the calling stack to the
// eventual handler. Each branch automatically has an error code of "errs.Undef"
// added to it, as well as an operation (package and function name string) set
// in the "Error.Opt" struct member.
// Branch example:
//
// err := Func()
// if err != nil {
// return errs.E(err)
// }
//
// The error code can be determined with the Kind function. Kind recursively
// unwraps the error to the leaf error and returns the error code. If no error
// code is defined in the leaf error, a type of "errs.Undef" is returned.
// Kind example:
//
// if errs.Kind(err) == errs.UserSyntax {
// // process syntax errors
// }
//
// To return a list of operations (package and function name as a string), use
// the Ops(err) to return a list of strings of every branch operation.
// Ops example:
//
// for _, op := range errs.Opt(err) {
// fmt.Println("operation: %s", op)
// }
func E(input ...any) *Error {
if len(input) == 0 {
return nil
}
info := call.CallInfo(1)
e := &Error{
Op: Op(info.Call),
}
for _, in := range input {
switch v := in.(type) {
case Code:
e.Kind = v
case error:
e.Err = v
case string:
e.Err = errors.New(v)
}
}
return e
}
// Ops returns a slice of all branch operations (functions), which the error
// passed through.
func Ops(err error) []Op {
result := []Op{}
if err == nil {
return result
}
e, ok := err.(*Error)
if !ok {
return result
}
result = append(result, e.Op)
childErr, ok := e.Err.(*Error)
if !ok {
return result
}
result = append(result, Ops(childErr)...)
return result
}
// Kind return the kind of error.
func Kind(err error) Code {
e, ok := err.(*Error)
if !ok {
return None
}
if e.Kind != 0 {
return e.Kind
}
return Kind(e.Err)
}
// Unwrap unwraps to the leaf error, or the bottom error in the recursive stack.
func Unwrap(err error) error {
parent, ok := err.(*Error)
if ok {
if parent.Kind > 0 {
return parent
}
return Unwrap(parent.Err)
}
return err
}
package errs
import (
"fmt"
"regexp"
"testing"
)
func leaf() error {
return E(App, `leaf`)
}
func branch1() error {
return E(leaf())
}
func branch2() error {
return E(branch1())
}
func branch3() error {
return E(branch2())
}
func TestE(t *testing.T) {
// simple error
errStr := `test error`
err := fmt.Errorf(errStr)
leaf := E(App, err)
if leaf.Kind != App {
t.Error(`kind does not match`)
}
if leaf.Err != err {
t.Error(`does not match original error`)
}
if !regexp.MustCompile(`\.TestE$`).MatchString(string(leaf.Op)) {
t.Error(`does not match function`)
}
re := regexp.MustCompile(`errs\.(?:branch3|branch2|branch1|leaf)`)
i := 0
for _, op := range Ops(branch3()) {
i++
if !re.MatchString(string(op)) {
t.Errorf(`invalid op match %s`, op)
}
}
if i != 4 {
t.Error(`expected 4 ops`)
}
if branch1().Error() != `leaf` {
t.Error(`did not recurse to leaf error`)
}
if Kind(branch1()) != App {
t.Error(`expected to have leaf error kind`)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment