From 9d7a288285683b1af8cd1ebb1a60c57210490265 Mon Sep 17 00:00:00 2001 From: Harri Lainio My Date: Thu, 7 Nov 2024 17:30:07 +0200 Subject: [PATCH] full error return tracing implementation (like in Zig) --- err2_test.go | 10 +++ internal/debug/debug.go | 19 ++++- internal/debug/debug_test.go | 152 ++++++++++++++++++++++++----------- internal/handler/handler.go | 30 +++++-- internal/tracer/tracer.go | 13 ++- samples/main-play.go | 53 ++++++++---- tracer.go | 40 +++++++-- 7 files changed, 237 insertions(+), 80 deletions(-) diff --git a/err2_test.go b/err2_test.go index 61c91fa..e3c1550 100644 --- a/err2_test.go +++ b/err2_test.go @@ -672,6 +672,16 @@ func TestSetErrorTracer(t *testing.T) { require.That(t, w == nil, "error tracer should be nil") } +func TestSetErrRetTracer(t *testing.T) { + t.Parallel() + w := err2.ErrRetTracer() + require.That(t, w == nil, "error return tracer should be nil") + var w1 io.Writer + err2.SetErrRetTracer(w1) + w = err2.ErrRetTracer() + require.That(t, w == nil, "error return tracer should be nil") +} + func ExampleCatch_withFmt() { // Set default logger to stdout for this example oldLogW := err2.LogTracer() diff --git a/internal/debug/debug.go b/internal/debug/debug.go index 8185dbe..f3197d5 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -26,6 +26,8 @@ type StackInfo struct { // these are used to filter out specific lines from output ExlRegexp []*regexp.Regexp + + PrintFirstOnly bool } var ( @@ -39,14 +41,19 @@ var ( // we want to check that this is not our package packageRegexp = regexp.MustCompile( - `^github\.com/lainio/err2[a-zA-Z0-9_/.\[\]]*\(`, + `^github\.com/lainio/err2[a-zA-Z0-9_/\.\[\]\@]*\(`, ) // testing package exluding regexps: testingPkgRegexp = regexp.MustCompile(`^testing\.`) testingFileRegexp = regexp.MustCompile(`^.*\/src\/testing\/testing\.go`) - exludeRegexps = []*regexp.Regexp{testingPkgRegexp, testingFileRegexp} + exludeRegexps = []*regexp.Regexp{testingPkgRegexp, testingFileRegexp} + exludeRegexpsAll = []*regexp.Regexp{ + testingPkgRegexp, + testingFileRegexp, + packageRegexp, + } ) func (si StackInfo) fullName() string { @@ -89,7 +96,11 @@ func (si StackInfo) canPrint(s string, anchorLine, i int) (ok bool) { // printed from call stack. anchorLine = 0 } - ok = i >= 2*si.Level+anchorLine + if si.PrintFirstOnly { + ok = i >= 2*si.Level+anchorLine && i < 2*si.Level+anchorLine+2 + } else { + ok = i >= 2*si.Level+anchorLine + } if si.ExlRegexp == nil { return ok @@ -271,7 +282,7 @@ func stackPrint(r io.Reader, w io.Writer, si StackInfo) { line := scanner.Text() // we can print a line if we didn't find anything, i.e. anchorLine is - // nilAnchor, which means that our start is not limited by then anchor + // nilAnchor, which means that our start is not limited by the anchor canPrint := anchorLine == nilAnchor // if it's not nilAnchor we need to check it more carefully if !canPrint { diff --git a/internal/debug/debug_test.go b/internal/debug/debug_test.go index a6b1748..c428198 100644 --- a/internal/debug/debug_test.go +++ b/internal/debug/debug_test.go @@ -20,23 +20,23 @@ func TestFullName(t *testing.T) { retval string } tests := []ttest{ - {"all empty", args{StackInfo{"", "", 0, nil, nil}}, ""}, + {"all empty", args{StackInfo{"", "", 0, nil, nil, false}}, ""}, { "namespaces", - args{StackInfo{"lainio/err2", "", 0, nil, nil}}, + args{StackInfo{"lainio/err2", "", 0, nil, nil, false}}, "lainio/err2", }, { "both", - args{StackInfo{"lainio/err2", "try", 0, nil, nil}}, + args{StackInfo{"lainio/err2", "try", 0, nil, nil, false}}, "lainio/err2.try", }, { "short both", - args{StackInfo{"err2", "Handle", 0, nil, nil}}, + args{StackInfo{"err2", "Handle", 0, nil, nil, false}}, "err2.Handle", }, - {"func", args{StackInfo{"", "try", 0, nil, nil}}, "try"}, + {"func", args{StackInfo{"", "try", 0, nil, nil, false}}, "try"}, } for _, ttv := range tests { tt := ttv @@ -61,34 +61,34 @@ func TestIsAnchor(t *testing.T) { tests := []ttest{ {"panic func and short regexp", args{ "github.com/lainio/err2.Return(0x14001c1ee20)", - StackInfo{"", "panic(", 0, PackageRegexp, nil}}, true}, + StackInfo{"", "panic(", 0, PackageRegexp, nil, false}}, true}, {"func hit and regexp on", args{ "github.com/lainioxx/err2_printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "printStackIf(", 0, noHitRegexp, nil}}, false}, + StackInfo{"", "printStackIf(", 0, noHitRegexp, nil, false}}, false}, {"short regexp no match", args{ "github.com/lainioxx/err2_printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, noHitRegexp, nil}}, false}, + StackInfo{"", "", 0, noHitRegexp, nil, false}}, false}, {"short regexp", args{ "github.com/lainio/err2/assert.That({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, PackageRegexp, nil}}, true}, + StackInfo{"", "", 0, PackageRegexp, nil, false}}, true}, {"short", args{ "github.com/lainio/err2.printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, nil, nil}}, true}, + StackInfo{"", "", 0, nil, nil, false}}, true}, {"short-but-false", args{ "github.com/lainio/err2.printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"err2", "Handle", 0, nil, nil}}, false}, + StackInfo{"err2", "Handle", 0, nil, nil, false}}, false}, {"medium", args{ "github.com/lainio/err2.Returnw(0x40000b3e60, {0x0, 0x0}, {0x0, 0x0, 0x0})", - StackInfo{"err2", "Returnw", 0, nil, nil}}, true}, + StackInfo{"err2", "Returnw", 0, nil, nil, false}}, true}, {"medium-but-false", args{ "github.com/lainio/err2.Returnw(0x40000b3e60, {0x0, 0x0}, {0x0, 0x0, 0x0})", - StackInfo{"err2", "Return(", 0, nil, nil}}, false}, + StackInfo{"err2", "Return(", 0, nil, nil, false}}, false}, {"long", args{ "github.com/lainio/err2.Handle(0x40000b3ed8, 0x40000b3ef8)", - StackInfo{"err2", "Handle", 0, nil, nil}}, true}, + StackInfo{"err2", "Handle", 0, nil, nil, false}}, true}, {"package name only", args{ "github.com/lainio/err2/try.To1[...](...)", - StackInfo{"lainio/err2", "", 0, nil, nil}}, true}, + StackInfo{"lainio/err2", "", 0, nil, nil, false}}, true}, } for _, ttv := range tests { tt := ttv @@ -113,28 +113,28 @@ func TestIsFuncAnchor(t *testing.T) { tests := []ttest{ {"func hit and regexp on", args{ "github.com/lainioxx/err2_printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "printStackIf(", 0, noHitRegexp, nil}}, true}, + StackInfo{"", "printStackIf(", 0, noHitRegexp, nil, false}}, true}, {"short regexp", args{ "github.com/lainio/err2/assert.That({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, PackageRegexp, nil}}, true}, + StackInfo{"", "", 0, PackageRegexp, nil, false}}, true}, {"short", args{ "github.com/lainio/err2.printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, nil, nil}}, true}, + StackInfo{"", "", 0, nil, nil, false}}, true}, {"short-but-false", args{ "github.com/lainio/err2.printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"err2", "Handle", 0, nil, nil}}, false}, + StackInfo{"err2", "Handle", 0, nil, nil, false}}, false}, {"medium", args{ "github.com/lainio/err2.Returnw(0x40000b3e60, {0x0, 0x0}, {0x0, 0x0, 0x0})", - StackInfo{"err2", "Returnw", 0, nil, nil}}, true}, + StackInfo{"err2", "Returnw", 0, nil, nil, false}}, true}, {"medium-but-false", args{ "github.com/lainio/err2.Returnw(0x40000b3e60, {0x0, 0x0}, {0x0, 0x0, 0x0})", - StackInfo{"err2", "Return(", 0, nil, nil}}, false}, + StackInfo{"err2", "Return(", 0, nil, nil, false}}, false}, {"long", args{ "github.com/lainio/err2.Handle(0x40000b3ed8, 0x40000b3ef8)", - StackInfo{"err2", "Handle", 0, nil, nil}}, true}, + StackInfo{"err2", "Handle", 0, nil, nil, false}}, true}, {"package name only", args{ "github.com/lainio/err2/try.To1[...](...)", - StackInfo{"lainio/err2", "", 0, nil, nil}}, true}, + StackInfo{"lainio/err2", "", 0, nil, nil, false}}, true}, } for _, ttv := range tests { tt := ttv @@ -285,50 +285,58 @@ func TestCalcAnchor(t *testing.T) { anchor int } tests := []ttest{ + { + "macOS from test using ALL regexp", + args{ + inputFromMac, + StackInfo{"", "StartPSM(", 1, nil, exludeRegexpsAll, false}, + }, + 16, + }, { "macOS from test using regexp", args{ inputFromMac, - StackInfo{"", "panic(", 1, PackageRegexp, nil}, + StackInfo{"", "panic(", 1, PackageRegexp, nil, false}, }, 12, }, - {"short", args{input, StackInfo{"", "panic(", 0, nil, nil}}, 6}, + {"short", args{input, StackInfo{"", "panic(", 0, nil, nil, false}}, 6}, { "short error stack", args{ inputByError, - StackInfo{"", "panic(", 0, PackageRegexp, nil}, + StackInfo{"", "panic(", 0, PackageRegexp, nil, false}, }, 4, }, { "short and nolimit", - args{input, StackInfo{"", "", 0, nil, nil}}, + args{input, StackInfo{"", "", 0, nil, nil, false}}, nilAnchor, }, { "short and only LVL is 2", - args{input, StackInfo{"", "", 2, nil, nil}}, + args{input, StackInfo{"", "", 2, nil, nil, false}}, 2, }, - {"medium", args{input1, StackInfo{"", "panic(", 0, nil, nil}}, 10}, + {"medium", args{input1, StackInfo{"", "panic(", 0, nil, nil, false}}, 10}, { "from test using panic", - args{inputFromTest, StackInfo{"", "panic(", 0, nil, nil}}, + args{inputFromTest, StackInfo{"", "panic(", 0, nil, nil, false}}, 8, }, { "from test", args{ inputFromTest, - StackInfo{"", "panic(", 0, PackageRegexp, nil}, + StackInfo{"", "panic(", 0, PackageRegexp, nil, false}, }, 14, }, { "macOS from test using panic", - args{inputFromMac, StackInfo{"", "panic(", 0, nil, nil}}, + args{inputFromMac, StackInfo{"", "panic(", 0, nil, nil, false}}, 12, }, } @@ -344,6 +352,48 @@ func TestCalcAnchor(t *testing.T) { } func TestStackPrint_limit(t *testing.T) { + t.Parallel() + type args struct { + input string + StackInfo + } + type ttest struct { + name string + args + output string + } + tests := []ttest{ + { + "find function with FRAME from test stack", + args{inputFromTest, + StackInfo{"", "", 8, nil, exludeRegexpsAll, true}}, + outputFromTestOnlyFunction, + }, + { + "find function with FRAME from mac stack", + args{inputFromMac, + StackInfo{"", "", 7, nil, exludeRegexpsAll, true}}, + outputFromMacOneFunction, + }, + } + for _, ttv := range tests { + tt := ttv + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := strings.NewReader(tt.input) + w := new(bytes.Buffer) + stackPrint(r, w, tt.StackInfo) + ins := strings.Split(tt.input, "\n") + outs := strings.Split(w.String(), "\n") + require.Thatf(t, len(ins) > len(outs), + "input length:%d should be greater:%d", len(ins), len(outs)) + b, a := tt.output, w.String() + require.Equal(t, a, b) + }) + } +} + +func TestStackPrint_OneFunction(t *testing.T) { t.Parallel() type args struct { input string @@ -357,47 +407,47 @@ func TestStackPrint_limit(t *testing.T) { tests := []ttest{ { "real test trace", - args{inputFromTest, StackInfo{"", "", 8, nil, exludeRegexps}}, + args{inputFromTest, StackInfo{"", "", 8, nil, exludeRegexps, false}}, outputFromTest, }, { "only level 4", - args{input1, StackInfo{"", "", 4, nil, nil}}, + args{input1, StackInfo{"", "", 4, nil, nil, false}}, output1, }, { "short", - args{input, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + args{input, StackInfo{"err2", "Returnw(", 0, nil, nil, false}}, output, }, { "medium", - args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil, false}}, output1, }, { "medium level 2", - args{input1, StackInfo{"err2", "Returnw(", 2, nil, nil}}, + args{input1, StackInfo{"err2", "Returnw(", 2, nil, nil, false}}, output12, }, { "medium level 0", - args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil, false}}, output1, }, { "medium panic", - args{input1, StackInfo{"", "panic(", 0, nil, nil}}, + args{input1, StackInfo{"", "panic(", 0, nil, nil, false}}, output1panic, }, { "long", - args{input2, StackInfo{"err2", "Handle(", 0, nil, nil}}, + args{input2, StackInfo{"err2", "Handle(", 0, nil, nil, false}}, output2, }, { "long lvl 2", - args{input2, StackInfo{"err2", "Handle(", 3, nil, nil}}, + args{input2, StackInfo{"err2", "Handle(", 3, nil, nil, false}}, output23, }, } @@ -434,35 +484,35 @@ func TestFuncName(t *testing.T) { tests := []ttest{ { "basic", - args{input2, StackInfo{"", "Handle", 1, nil, nil}}, + args{input2, StackInfo{"", "Handle", 1, nil, nil, false}}, "err2.ReturnW", 214, 6, }, { "basic lvl 3", - args{input2, StackInfo{"", "Handle", 3, nil, nil}}, + args{input2, StackInfo{"", "Handle", 3, nil, nil, false}}, "err2.ReturnW", 214, 6, }, { "basic lvl 2", - args{input2, StackInfo{"lainio/err2", "Handle", 1, nil, nil}}, + args{input2, StackInfo{"lainio/err2", "Handle", 1, nil, nil, false}}, "err2.ReturnW", 214, 6, }, { "method", - args{inputFromTest, StackInfo{"", "Handle", 1, nil, nil}}, + args{inputFromTest, StackInfo{"", "Handle", 1, nil, nil, false}}, "ssi.(*DIDAgent).AssertWallet", 146, 8, }, { "pipeline", - args{inputPipelineStack, StackInfo{"", "Handle", -1, nil, nil}}, + args{inputPipelineStack, StackInfo{"", "Handle", -1, nil, nil, false}}, "CopyFile", 29, 9, @@ -517,6 +567,11 @@ created by github.com/findy-network/findy-agent/agent/prot.FindAndStartTask /Users/harrilainio/go/src/github.com/findy-network/findy-agent/agent/prot/processor.go:337 +0x21c ` + outputFromMacOneFunction = `goroutine 518 [running]: +github.com/findy-network/findy-agent/agent/cloud.(*Agent).PwPipe(0x1400024e2d0, {0x140017ad770, 0x24}) + /Users/harrilainio/go/src/github.com/findy-network/findy-agent/agent/cloud/agent.go:202 +0x324 +` + inputFromTest = `goroutine 31 [running]: testing.tRunner.func1.2({0xa8e0e0, 0x40001937d0}) /usr/local/go/src/testing/testing.go:1389 +0x1c8 @@ -557,6 +612,11 @@ github.com/findy-network/findy-agent/agent/sec_test.TestPipe_packPeer(0x4000106d /home/god/go/src/github.com/findy-network/findy-agent/agent/sec/pipe_test.go:355 +0x1b8 ` + outputFromTestOnlyFunction = `goroutine 31 [running]: +github.com/findy-network/findy-agent/agent/ssi.(*DIDAgent).AssertWallet(...) + /home/god/go/src/github.com/findy-network/findy-agent/agent/ssi/agent.go:146 +` + inputByError = `goroutine 1 [running]: panic({0x137b20, 0x400007ac60}) /usr/local/go/src/runtime/panic.go:838 +0x20c diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 235dc23..3e173c3 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -94,11 +94,20 @@ func (i *Info) callNilHandler() { } func (i *Info) checkErrorTracer() { + errRet := false + if i.ErrorTracer == nil { + i.ErrorTracer = tracer.ErrRet.Tracer() + errRet = true + } if i.ErrorTracer == nil { i.ErrorTracer = tracer.Error.Tracer() + errRet = false } if i.ErrorTracer != nil { - si := stackPrologueError + si := stackPrologueErrRet + if !errRet { + si = stackPrologueError + } if i.Any == nil { i.Any = i.safeErr() } @@ -405,18 +414,29 @@ see 'err2/scripts/README.md' and run auto-migration scripts for your repo func printStack(w io.Writer, si debug.StackInfo, msg any) { fmt.Fprintf(w, "---\n%v\n---\n", msg) debug.FprintStack(w, si) + if si.PrintFirstOnly { + fmt.Fprintln(w, "") + } } var ( - // stackPrologueRuntime = newSI("", "panic(", 1) - stackPrologueError = newErrSI() - stackProloguePanic = newSI("", "panic(", 1) + stackPrologueError = newErrSIOld() + stackPrologueErrRet = newErrSI() + stackProloguePanic = newSI("", "panic(", 1) ) -func newErrSI() debug.StackInfo { +func newErrSIOld() debug.StackInfo { return debug.StackInfo{Regexp: debug.PackageRegexp, Level: 1} } +func newErrSI() debug.StackInfo { + return debug.StackInfo{ + Level: 1, + Regexp: debug.PackageRegexp, + PrintFirstOnly: true, + } +} + func newSI(pn, fn string, lvl int) debug.StackInfo { return debug.StackInfo{ PackageName: pn, diff --git a/internal/tracer/tracer.go b/internal/tracer/tracer.go index aa10296..53f8fbe 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -19,12 +19,14 @@ type writer struct { } var ( - Error value - Panic value - Log value + Error value + Panic value + Log value + ErrRet value ) func init() { + ErrRet.SetTracer(nil) Error.SetTracer(nil) // Because we stop panics as default, we need to output as default Panic.SetTracer(os.Stderr) @@ -39,6 +41,11 @@ func init() { "`stream` for error tracing: stderr, stdout", ) flag.Var(&Panic, "err2-panic-trace", "`stream` for panic tracing") + flag.Var( + &ErrRet, + "err2-ret-trace", + "`stream` for error return tracing: stderr, stdout", + ) } func (v *value) Tracer() io.Writer { diff --git a/samples/main-play.go b/samples/main-play.go index 9284034..f06dc81 100644 --- a/samples/main-play.go +++ b/samples/main-play.go @@ -78,7 +78,7 @@ func TryCopyFile(src, dst string) { r := try.To1(os.Open(src)) defer r.Close() - w:= try.To1(os.Create(dst)) + w := try.To1(os.Create(dst)) defer err2.Handle(&err, func(err error) error { try.Out(os.Remove(dst)).Logf("cleaning error") return err @@ -94,18 +94,37 @@ func CallRecur(d int) (ret int, err error) { } func doRecur(d int) (ret int, err error) { - d-- if d >= 0 { // Keep below to show how asserts work //assert.NotZero(d) // Comment out the above assert statement to simulate runtime-error ret = 10 / d - fmt.Println(ret) + fmt.Println("ret:", ret) //return doRecur(d) } return ret, fmt.Errorf("root error") } +type runMode int + +const ( + runModePlay runMode = iota + runModePlayRec +) + +func (rm runMode) String() string { + return []string{"play", "play-recursion"}[rm] +} + +var rMode runMode + +func setRunMode() { + playRec := runModePlayRec + if *mode == playRec.String() { + rMode = runModePlayRec + } +} + func doPlayMain() { // Keep here that you can play without changing imports assert.That(true) @@ -122,11 +141,7 @@ func doPlayMain() { // errors are caught without specific handlers. defer err2.Catch(err2.Stderr) - // If you don't want to use tracers or you just need a proper error handler - // here. - // defer err2.Catch(func(err error) { - // fmt.Println("ERROR:", err) - // }) + setRunMode() // by calling one of these you can test how automatic logging in above // catch works correctly: the last source of error check is shown in line @@ -161,18 +176,22 @@ func doMain() (err error) { // Both source and destination don't exist //TryCopyFile("/notfound/path/file.go", "/notfound/path/file.bak") - // to play with real args: - TryCopyFile(flag.Arg(0), flag.Arg(1)) - if len(flag.Args()) > 0 { - // Next fn demonstrates how error and panic traces work, comment out all - // above CopyFile calls to play with: - argument := try.To1(strconv.Atoi(flag.Arg(0))) - ret := try.To1(CallRecur(argument)) - fmt.Println("ret val:", ret) + if rMode == runModePlayRec { + // Next fn demonstrates how error and panic traces work, comment + // out all above CopyFile calls to play with: + argument := try.To1(strconv.Atoi(flag.Arg(0))) + ret := try.To1(CallRecur(argument)) + fmt.Println("ret val:", ret) + } else { + // to play with real args: + //TryCopyFile(flag.Arg(0), flag.Arg(1)) + try.To(CopyFile(flag.Arg(0), flag.Arg(1))) + } } else { // 2nd argument is empty to assert - TryCopyFile("main.go", "") + //TryCopyFile("main.go", "") + try.To(CopyFile("main.go", "")) } fmt.Println("=== you cannot see this ===") diff --git a/tracer.go b/tracer.go index 954768f..f5a96f3 100644 --- a/tracer.go +++ b/tracer.go @@ -8,10 +8,22 @@ import ( // ErrorTracer returns current [io.Writer] for automatic error stack tracing. // The default value is nil. +// +// See [SetErrRetTracer] and [SetErrorTracer] for more information of the two +// different error tracing systems. func ErrorTracer() io.Writer { return tracer.Error.Tracer() } +// ErrRetTracer returns current [io.Writer] for automatic error return stack +// tracing. The default value is nil. +// +// See [SetErrRetTracer] and [SetErrorTracer] for more information of the two +// different error tracing systems. +func ErrRetTracer() io.Writer { + return tracer.Error.Tracer() +} + // PanicTracer returns current [io.Writer] for automatic panic stack tracing. Note // that [runtime.Error] types which are transported by panics are controlled by // this. The default value is [os.Stderr]. @@ -36,18 +48,36 @@ func LogTracer() io.Writer { // Error trace is almost the same format as Go's standard call stack but it may // have multiple sections because every [Handle] and [Catch] prints it. If an // error happens in a deep call stack, the error trace includes various parts. -// The principle is similar to [Zig Error Return Traces], where you see how -// error bubbles up. However, our error trace is a combination of error return -// traces and stack traces because we get all the needed information at once. +// If you prefer similar to [Zig Error Return Traces], where you see how +// error bubbles up, you should use [SetErrRetTrace]. // // Remember that you can reset these with [flag] package support. See // documentation of err2 package's flag section. -// -// [Zig Error Return Traces]: https://ziglang.org/documentation/master/#Error-Return-Traces func SetErrorTracer(w io.Writer) { tracer.Error.SetTracer(w) } +// SetErrRetTracer sets a [io.Writer] for automatic error return stack tracing. +// The err2 default is nil. Note that any function that has deferred [Handle] or +// [Catch] is capable to print error return stack trace: +// +// func CopyFile(src, dst string) (err error) { +// defer err2.Handle(&err) // <- makes error trace printing decision +// +// Error return trace is almost the same format as Go's standard call stack but +// it may have multiple sections because every [Handle] and [Catch] prints it. +// If an error happens in a deep call stack, the error return trace includes +// various parts. The principle is similar to [Zig Error Return Traces], where +// you see how error bubbles up. +// +// Remember that you can reset these with [flag] package support. See +// documentation of err2 package's flag section. +// +// [Zig Error Return Traces]: https://ziglang.org/documentation/master/#Error-Return-Traces +func SetErrRetTracer(w io.Writer) { + tracer.ErrRet.SetTracer(w) +} + // SetPanicTracer sets a [io.Writer] for automatic panic stack tracing. The err2 // default is [os.Stderr]. Note that [runtime.Error] types which are transported by // panics are controlled by this. Note also that the current function is capable