Skip to content

Commit

Permalink
report global error initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
pierrre committed Oct 11, 2024
1 parent 01d3ff4 commit a180a1e
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 1 deletion.
58 changes: 57 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// Package errors provides error management.
//
// By convention, wrapping functions return a nil error if the given error is nil.
// By convention, wrapping functions return a nil error if the provided error is nil.
package errors

import (
std_errors "errors"
"runtime"
"strings"
"testing"

"github.com/pierrre/errors/errbase"
"github.com/pierrre/errors/errmsg"
Expand All @@ -21,6 +24,9 @@ var ErrUnsupported = std_errors.ErrUnsupported
func New(msg string) error {
err := errbase.New(msg)
err = errstack.WrapSkip(err, 1)
if ReportGlobalInit != nil {
checkGlobalInit(err, ReportGlobalInit)
}
return err //nolint: wrapcheck // The error is wrapped.
}

Expand All @@ -30,9 +36,59 @@ func New(msg string) error {
func Newf(format string, args ...any) error {
err := errbase.Newf(format, args...)
err = errstack.WrapSkip(err, 1)
if ReportGlobalInit != nil {
checkGlobalInit(err, ReportGlobalInit)
}
return err //nolint: wrapcheck // The error is wrapped.
}

// ReportGlobalInit reports a global error initialization.
// It is discouraged to call [New] or [Newf] to create a global error, as it will contain the stack of the init goroutine that created it.
// Instead, call [errbase.New] or [errbase.Newf], and [Wrap] it before returning it, which will add the stack of the goroutine returning the error.
//
// Example:
//
// var ErrGlobal = errbase.New("global error")
//
// func myFunc() error {
// return errors.Wrap(ErrGlobal, "myFunc error")
// }
//
// The default values's behavior is to panic during tests, and do nothing during normal execution.
// It can be disabled by setting it to nil.
//
// The implementation of [New] and [Newf] checks if the error is created by a function named "init".
// It doesn't report errors created by "init()" functions, which are named "init.N" where N is a number.
var ReportGlobalInit func(error) = func() func(error) {
var f func(error)
if testing.Testing() {
f = func(err error) {
panic(err)
}
}
return f
}()

func checkGlobalInit(err error, report func(error)) {
// This code doesn't call [errstack.Frames] to avoid memory allocations.
errf, ok := err.(interface {
StackFrames() []uintptr
})
if !ok {
return
}
pcs := errf.StackFrames()
if len(pcs) == 0 {
return
}
f := runtime.FuncForPC(pcs[0])
if !strings.HasSuffix(f.Name(), ".init") {
return
}
err = Wrap(err, "global error initialization detected, use errbase.New() instead, see https://pkg.go.dev/github.com/pierrre/errors#ReportGlobalInit ")
report(err)
}

// Wrap adds a message to an error, and a stack if it doesn't have one.
func Wrap(err error, msg string) error {
err = errstack.EnsureSkip(err, 1)
Expand Down
5 changes: 5 additions & 0 deletions errors_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package errors

func CheckGlobalInit(err error, report func(error)) {
checkGlobalInit(err, report)
}
32 changes: 32 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,38 @@ func TestNewf(t *testing.T) {
assert.SliceLen(t, sfs, 1)
}

func TestReportGlobalInitPanics(t *testing.T) {
assert.Panics(t, func() {
ReportGlobalInit(New("error"))
})
}

var errGlobal = errstack.Wrap(errbase.New("global error"))

func TestCheckGlobalInit(t *testing.T) {
called := false
CheckGlobalInit(errGlobal, func(err error) {
called = true
assert.ErrorEqual(t, err, "global error initialization detected, use errbase.New() instead, see https://pkg.go.dev/github.com/pierrre/errors#ReportGlobalInit : global error")
})
assert.True(t, called)
}

func TestCheckGlobalInitNoStack(t *testing.T) {
err := errbase.New("error")
CheckGlobalInit(err, nil)
}

func TestCheckGlobalInitEmptyPCs(t *testing.T) {
err := errstack.WrapSkip(errbase.New("error"), 1000)
CheckGlobalInit(err, nil)
}

func TestCheckGlobalInitNotInit(t *testing.T) {
err := New("error")
CheckGlobalInit(err, nil)
}

func TestWrap(t *testing.T) {
err := errbase.New("error")
err = Wrap(err, "test")
Expand Down

0 comments on commit a180a1e

Please sign in to comment.