package errors_test import ( "encoding/json" "fmt" "testing" go_errors "errors" pkg_errors "github.com/pkg/errors" uf_errors "gitlab.com/uafrica/go-utils/errors" ) //use this struct to define various types of errors to experiment with type Test struct { title string err error } var tests []Test //define the errors for testing //in each case, the main error message is "<this is broken>" //in some cases that is passed up and wrapped with more messages //the tests use different combinations of go errors, fmt.Errorf(), github.com/pkg/errors and our own go-utils/errors library //NOTE: I put all the new calls on new lines to report different line numbers in the stack... func init() { tests = []Test{ //a very simple go "errors.New()" error - does not have a stack {title: "go.New(...)", err: go_errors.New("<this is broken>")}, //a fmt.Errorf() error - does not have a stack {title: "fmt.Errorf(...)", err: fmt.Errorf("<this is broken>")}, //a better github.com/pkg/errors.New() error that includes the call stack {title: "pkg.New(...)", err: pkg_errors.New("<this is broken>")}, //our own error type that always records the caller: {title: "uafrica.New(...)", err: uf_errors.New("<this is broken>")}, //one can use pkg WithStack() to add a stack to an error that does not have a stack {title: "pkg.WithStack(go.New(...))", err: pkg_errors.WithStack( go_errors.New("<this is broken>"), )}, //if you are ok with pkg's way to add a stack manually (I am not), //then you could easily make this mistake to add a stack to an error that already has a stack //by default pkg then prints two stacks and the rest of the error messages are after the last stack //making it very hard to figure out what went wrong and where... {title: "pkg.WithStack(pkg.New())", err: pkg_errors.WithStack( pkg_errors.New("<this is broken>"), )}, //one can also add a stack with our own errors, //note that we always want an explanation which is just to enforce good practice: {title: "uafrica.Wrapf(go.New(...))", err: uf_errors.Wrapf( go_errors.New("<this is broken>"), "<it is not working>", )}, //and when we wrap an error that already has a stack, it will not duplicate, //note that we always want an explanation which is just to enforce good practice: {title: "uafrica.Wrapf(pkg.New(...))", err: uf_errors.Wrapf( pkg_errors.New("<this is broken>"), "<it is not working>", )}, //pkg can also wrap an error with a text explanation, //but the synctax is annoying as one has to add the stack and the message separately //(compared to a simple Wrapf() call to imply a stack as well...) //we firstly wrap an error that has NO stack: {title: "pkg.WithMessage(pkg.WithStack(go.New()))", err: pkg_errors.WithMessage( pkg_errors.WithStack( go_errors.New("<this is broken>"), ), "<it is not working>", )}, //we will typically pass an error up several layers before it is reported //and the original error may come without a stack, //and another package might have added a stack using pkg //then we still add our own explanations on top of that //in both the following two examples, we have the following stack: // // db not open (without a stack) // query failed (without a stack, using fmt.Errorf("query failed: %v", err)) // failed to read list (stack added with pkg.WithStack(), then message added with pkg.WithMessage()) // failed to read users from db (demonstrated with pkg.WithMessage() and our own Wrapf()) // cannot display list of users (demonstrated with pkg.WithMessage() and our own Wrapf()) // //first using only pkg: {title: "full stack with pkg", err: pkg_errors.WithMessage( pkg_errors.WithMessage( pkg_errors.WithStack( //assuming this comes from 3rd party lib e.g. sql or pg - we cannot change this or deeper errors pkg_errors.WithMessage( fmt.Errorf( "query failed: %v", go_errors.New("db not open"), ), "failed to read list", ), ), "failed to read users from db", ), "cannot display list of users", )}, //same thing but wrapping at the top with out own messages and stack {title: "full stack with uafrica", err: uf_errors.Wrapf( uf_errors.Wrapf( pkg_errors.WithStack( //assuming this comes from 3rd party lib e.g. sql or pg - we cannot change this or deeper errors pkg_errors.WithMessage( fmt.Errorf( "query failed: %v", go_errors.New("db not open"), ), "failed to read list", ), ), "failed to read users from db", ), "cannot display list of users", )}, //============================================================================================================================================ //in the final full stack case, here is the comparison: //============================================================================================================================================ // OUTPUT OF err.Errorf() // ---------------------- // // pkg: cannot display list of users: failed to read users from db: failed to read list: query failed: db not open // // uafrica: cannot display list of users, because failed to read users from db, because failed to read list: query failed: db not open // // (at the end, uafrica could not insert ", because ..." because the deepest level was just text from fmt.Errorf(...)) //============================================================================================================================================ // OUTPUT OF Printf("%s") for simple error // --------------------------------------- // // pkg: cannot display list of users: failed to read users from db: failed to read list: query failed: db not open // // uafrica: cannot display list of users // // Note that pkg has no simple way to just display the top level error, while we can do it with formatting directive "%s" //============================================================================================================================================ // OUTPUT OF Printf("%+v") for full error stack // -------------------------------------------- // // pkg: // query failed: db not open // failed to read list // command-line-arguments_test.init.0 // /Users/jansemmelink/uafrica/go-utils/errors/other_test.go:96 // runtime.doInit // /usr/local/go/src/runtime/proc.go:6498 // runtime.doInit // /usr/local/go/src/runtime/proc.go:6475 // runtime.main // /usr/local/go/src/runtime/proc.go:238 // runtime.goexit // /usr/local/go/src/runtime/asm_arm64.s:1133 // failed to read users from db // cannot display list of users // // uafrica: // command-line-arguments_test.init/other_test.go(111): 0() cannot display list of users, because // command-line-arguments_test.init/other_test.go(112): 0() failed to read users from db, because // command-line-arguments_test.init.0/other_test.go(113): init.0(): failed to read list: query failed: db not open // // Difference: // - pkg starts with the lowest level then what effects it had (opposite order from how it prints err.Error()) // uafrica maintains the order of effect then the reason why, which kind of makes more sense // - pkg write multiple lines for each stack entry, and does not keep the messages to the stack lines // uafrica put the error message next to the stack line that wrote it // - pkg wrapping with text, does not include the stack again, so line 94 and 95 that passed the error on, is not mentioned // uafrica mention each line that handled the error: 111, 112, 113 (called libs in line 114..117 did not include the stack) // - pkg reports full package and FULL filename (host path where the code was compiled) which is not useful in production // uafrica reports the package and BASE filename and function name instead. //============================================================================================================================================ // Compact Stack: // -------------- // In streaming log files we do not want a multi-line display of error messages, so uafrica also supports "%c" and "%+c" formatting to print // on one line with ';' instead of new lines. The '+' adds the source package, file, line and function name. // // "%c" is same as err.Error(): // // cannot display list of users, because failed to read users from db, because failed to read list: query failed: db not open // // "%+c" is that plus the source references: // // command-line-arguments_test.init/error_formats_test.go(111): 0() cannot display list of users, because command-line-arguments_test.init/error_formats_test.go(112): 0() failed to read users from db, because command-line-arguments_test.init.0/error_formats_test.go(113): init.0(): failed to read list: query failed: db not open // // "%v" is same as "%c" over multiple lines: // // cannot display list of users, because // failed to read users from db, because // failed to read list: query failed: db not open // // "%+v" is same as "%+c" over multiple lines: (as in previous section where compared with pkg): // // command-line-arguments_test.init/error_formats_test.go(111): 0() cannot display list of users, because // command-line-arguments_test.init/error_formats_test.go(112): 0() failed to read users from db, because // command-line-arguments_test.init.0/error_formats_test.go(113): init.0(): failed to read list: query failed: db not open // //============================================================================================================================================ // JSON: // ----- // Having our own errors package allows us also flexibility to do things like creating a JSON error description. // This is illustrated in the JSON output: // {"error":"cannot display list of users","source":{"package":"command-line-arguments_test.init","file":"other_test.go","line":111,"function":"0"}, // "cause":{"error":"failed to read users from db","source":{"package":"command-line-arguments_test.init","file":"other_test.go","line":112, // "function":"0"},"cause":{"error":"failed to read list: query failed: db not open","source":{"package":"command-line-arguments_test.init.0", // "file":"other_test.go","line":113,"function":"init.0"},"cause":{"error":"failed to read list: query failed: db not open","cause":{ // "error":"query failed: db not open"}}}}} // Which we can pretty print with jq into a very readable stack: // // { // "error": "cannot display list of users", // "source": { // "package": "command-line-arguments_test.init", // "file": "other_test.go", // "line": 111, // "function": "0" // }, // "cause": { // "error": "failed to read users from db", // "source": { // "package": "command-line-arguments_test.init", // "file": "other_test.go", // "line": 112, // "function": "0" // }, // "cause": { // "error": "failed to read list: query failed: db not open", // "source": { // "package": "command-line-arguments_test.init.0", // "file": "other_test.go", // "line": 113, // "function": "init.0" // }, // "cause": { // "error": "failed to read list: query failed: db not open", // "cause": { // "error": "query failed: db not open" // } // } // } // } // } //============================================================================================================================================ } } func TestErrorFormats(t *testing.T) { for _, test := range tests { show(t, test.title, test.err) } } func show(t *testing.T, title string, err error) { t.Logf("------------------------------------------------------------------------------------") t.Logf("-----[ %-70.70s ]-----", title) t.Logf("------------------------------------------------------------------------------------") t.Logf("err.Error(): %s", err.Error()) t.Logf("Printf(%%s): %s", err) if _, ok := err.(*uf_errors.CustomError); ok { //compact only supported on our own lib: t.Logf("Printf(%%c): %c", err) t.Logf("Printf(%%+c): %+c", err) } t.Logf("Printf(%%v): %v", err) t.Logf("Printf(%%+v): %+v", err) if e, ok := err.(*uf_errors.CustomError); ok { //description only supported on our own lib: desc := e.Description() jsonDesc, _ := json.Marshal(desc) t.Logf("JSON: %s", string(jsonDesc)) } }