Last active
May 3, 2024 18:26
-
-
Save dexterp/1edee19aa54d87f33d3cc727fde86196 to your computer and use it in GitHub Desktop.
GO Error wrapping in Go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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