Skip to content

Commit

Permalink
Improve errors documentation
Browse files Browse the repository at this point in the history
Also add JoinsG
  • Loading branch information
Greg Weber committed Dec 22, 2024
1 parent c4dadbc commit 369f7a5
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 78 deletions.
8 changes: 4 additions & 4 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ func Wrap(err error, message string) error {
}

// Wrapf returns an error annotating err with a stack trace
// at the point Annotatef is call, and the format specifier.
// If err is nil, Annotatef returns nil.
// at the point Wrapf is call, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
if IsNil(err) {
return nil
Expand All @@ -187,9 +187,9 @@ func Wrapf(err error, format string, args ...interface{}) error {
}
}

// WrapoNoStack annotates err with a new message.
// If err is nil, returns nil.
// WrapNoStack does not add a new stack trace.
// WrapNoStack annotates err with a new message.
// If err is nil, returns nil.
// When used consecutively, it will append the message strings rather than creating a new error
func WrapNoStack(err error, message string) error {
if IsNil(err) {
Expand Down
219 changes: 150 additions & 69 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package errors_test

import (
stderrors "errors"
"fmt"
"strings"

Expand All @@ -10,19 +11,26 @@ import (
func ExampleNew() {
err := errors.New("whoops")
fmt.Println(err)

// Output: whoops
}

func firstLines(s string) string {
allLines := strings.SplitAfterN(s, "\n", 10)
lines := allLines[0:1]
for _, line := range allLines[1 : len(allLines)-2] {
// function
// Returns the first 'n' lines of a given string, where each line is separated by '\n'.
func firstNLines(s string, n int) string {
allLines := strings.SplitN(s, "\n", n+1)
if n > len(allLines) {
n = len(allLines)
}
return strings.Join(allLines[0:n], "\n")
}

func functionLines(s string) string {
lines := []string{}
for _, line := range strings.SplitAfter(s, "\n") {
// function name
if strings.HasPrefix(line, "github.com") {
lines = append(lines, line)
// File (different on different machine)
} else if strings.HasPrefix(line, "\t") {
// File name (different on different machine)
} else if strings.HasPrefix(line, "\t") || len(lines) == 0 {
continue
} else {
break
Expand All @@ -31,122 +39,104 @@ func firstLines(s string) string {
return strings.Join(lines, "")
}

func ExampleNew_printf() {
err := errors.Errorf("whoops, %d", 2)
fmt.Print(firstLines(fmt.Sprintf("%+v", err)))

// Output:
// whoops, 2
// github.com/gregwebs/errors_test.ExampleNew_printf
}

func ExampleWrapNoStack() {
cause := errors.New("whoops")
cause := stderrors.New("whoops")
err := errors.WrapNoStack(cause, "oh noes")
fmt.Println(err)

// Output: oh noes: whoops
fmt.Printf("%+v", err)
// Output:
// whoops
// oh noes
}

func ExampleAddStack() {
cause := errors.New("whoops")
// stderrors is the standard library errors: no stack trace
cause := stderrors.New("whoops")
fmt.Print(firstNLines(fmt.Sprintf("%+v\n", cause), 2))
// Add a stack trace to it
err := errors.AddStack(cause)
fmt.Println(err)

// Output: whoops
fmt.Print(firstNLines(fmt.Sprintf("%+v\n", err), 2))
// Output:
// whoops
// whoops
// github.com/gregwebs/errors_test.ExampleAddStack
}

func ExampleAddStack_printf() {
cause := errors.New("whoops")
err := errors.AddStack(cause)
fmt.Print(firstLines(fmt.Sprintf("%+v", err)))
func ExampleAddStackSkip() {
// stderrors is the standard library errors: no stack trace
inner := func() {
cause := stderrors.New("whoops")
err := errors.AddStack(cause)
fmt.Print(functionLines(fmt.Sprintf("%+v\n", err)))

fmt.Println("---")

// Add a stack trace to it
err = errors.AddStackSkip(cause, 1)
fmt.Print(functionLines(fmt.Sprintf("%+v\n", err)))
}
inner()
// Output:
// whoops
// github.com/gregwebs/errors_test.ExampleAddStack_printf
// github.com/gregwebs/errors_test.ExampleAddStackSkip.func1
// github.com/gregwebs/errors_test.ExampleAddStackSkip
// ---
// github.com/gregwebs/errors_test.ExampleAddStackSkip
}

func ExampleWrap() {
cause := errors.New("whoops")
err := errors.Wrap(cause, "oh noes")
fmt.Println(err)

// Output: oh noes: whoops
}

func fn() error {
e1 := errors.New("error")
func newWrappedErr() error {
e1 := errors.New("cause")
e2 := errors.Wrap(e1, "inner")
e3 := errors.Wrap(e2, "middle")
return errors.Wrap(e3, "outer")
}

func ExampleCause() {
err := fn()
err := newWrappedErr()
fmt.Println(err)
fmt.Println(errors.Cause(err))

// Output: outer: middle: inner: error
// error
}

func ExampleWrap_extended() {
err := fn()
fmt.Print(firstLines(fmt.Sprintf("%+v", err)))

// output:
// error
// github.com/gregwebs/errors_test.fn
// github.com/gregwebs/errors_test.ExampleWrap_extended
// Output: outer: middle: inner: cause
// cause
}

func ExampleWrapf() {
cause := errors.New("whoops")
err := errors.Wrapf(cause, "oh noes #%d", 2)
fmt.Println(err)

// Output: oh noes #2: whoops
}

func ExampleErrorf_extended() {
func ExampleErrorf() {
err := errors.Errorf("whoops: %s", "foo")
fmt.Print(firstLines(fmt.Sprintf("%+v", err)))

verbose := fmt.Sprintf("%+v", err)
fmt.Print(strings.Join(strings.SplitN(verbose, "\n", 3)[0:2], "\n"))
// Output:
// whoops: foo
// github.com/gregwebs/errors_test.ExampleErrorf_extended
// github.com/gregwebs/errors_test.ExampleErrorf
}

func Example_stackTrace() {
type stackTracer interface {
StackTrace() errors.StackTrace
}

err, ok := errors.Cause(fn()).(stackTracer)
err, ok := errors.Cause(newWrappedErr()).(stackTracer)
if !ok {
panic("oops, err does not implement stackTracer")
}

st := err.StackTrace()
fmt.Print(firstLines(fmt.Sprintf("%+v", st)))

fmt.Print(functionLines(fmt.Sprintf("%+v", st)))
// Output:
// github.com/gregwebs/errors_test.fn
// github.com/gregwebs/errors_test.newWrappedErr
// github.com/gregwebs/errors_test.Example_stackTrace
}

func ExampleCause_printf() {
err := errors.Wrap(func() error {
return func() error {
return errors.Errorf("hello %s", "world")
}()
}(), "failed")

fmt.Printf("%v", err)

// Output: failed: hello world
}

func ExampleStructuredError() {
err := errors.Wraps(
errors.New("cause"),
Expand Down Expand Up @@ -183,3 +173,94 @@ func ExampleSlogRecord() {
// Output:
// structured: cause
}

type ErrEmpty struct{}

func (et ErrEmpty) Error() string {
return "empty"
}

func ExampleAsType() {
err := errors.Wrap(ErrEmpty{}, "failed")
cause, _ := errors.AsType[ErrEmpty](err)
fmt.Printf("%v", cause)
// Output: empty
}

type errorGroup struct {
errs []error
}

func (eg *errorGroup) Error() string {
return errors.Join(eg.errs...).Error()
}

func (eg *errorGroup) Errors() []error { return eg.errs }

func ExampleErrors() {
var eg errorGroup
eg.errs = append(eg.errs, errors.New("error1"))
eg.errs = append(eg.errs, errors.New("error2"))

fmt.Println(errors.Errors(nil))
fmt.Println(errors.Errors(errors.New("test")))
fmt.Println(errors.Errors(&eg))
// Output:
// []
// []
// [error1 error2]
}

func ExampleIsNil() {
var empty *ErrEmpty = nil //nolint:staticcheck
var err error = empty
fmt.Println(err == nil) //nolint:staticcheck
fmt.Println(errors.IsNil(err))
// Output:
// false
// true
}

type inplace struct {
*errors.ErrorWrap
}

func ExampleWrapInPlace() {
err := inplace{errors.NewErrorWrap(errors.New("original error"))}

// Wrap the error in place
wrapped := errors.WrapInPlace(err, errors.WrapFn("wrapped"))

// Print the error and whether it was wrapped in place
fmt.Printf("Wrapped in place: %v\n", wrapped)
fmt.Printf("Error: %v\n", err)

// Try with a regular error that doesn't implement ErrorWrapper
regularErr := errors.New("regular error")
wrapped = errors.WrapInPlace(regularErr, errors.WrapFn("wrapped"))

// Print the result for regular error
fmt.Printf("Regular error wrapped in place: %v\n", wrapped)
fmt.Printf("Regular error: %v\n", regularErr)

// Output:
// Wrapped in place: true
// Error: wrapped: original error
// Regular error wrapped in place: false
// Regular error: regular error
}

func ExampleWraps() {
// Create a base error
baseErr := fmt.Errorf("database connection failed")

// Wrap the error with additional structured information
wrappedErr := errors.Wraps(baseErr, "user authentication failed",
"user_id", "123",
"attempt", 3,
)

// Print the error
fmt.Println(wrappedErr)
// Output: user authentication failed user_id=123 attempt=3: database connection failed
}
22 changes: 22 additions & 0 deletions go_123_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package errors

import (
"errors"
"fmt"
"io"
"testing"
)
Expand Down Expand Up @@ -92,3 +93,24 @@ func TestFind(t *testing.T) {
}
}
}

func ExampleUnwrapGroups() {
err1 := New("error 1")
err2 := New("error 2")
group := Join(err1, err2)
wrapped := Wrap(group, "wrapped")

for e := range UnwrapGroups(wrapped) {
fmt.Println(e.Error() + "\n")
}
// Output:
// wrapped: error 1
// error 2
//
// error 1
// error 2
//
// error 1
//
// error 2
}
12 changes: 8 additions & 4 deletions group.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ func Join(errs ...error) error {
return stderrors.Join(errs...)
}

// The same as errors.Join but does not wrap the array in an error
// Also uses generics to retain the type
// Also uses IsNil for a better nil check
func Joins[T error](errs ...T) []T {
// A generic form of Joins
func JoinsG[T error](errs ...T) []T {
n := 0
for _, err := range errs {
if !IsNil(err) {
Expand All @@ -29,6 +27,12 @@ func Joins[T error](errs ...T) []T {
return newErrs
}

// The same as errors.Join but returns the array rather than wrapping it.
// Also uses IsNil for a better nil check.
func Joins(errs ...error) []error {
return JoinsG(errs...)
}

// errorGroup is an interface for multiple errors that are not a chain.
// This happens for example when executing multiple operations in parallel.
// The standard Go API is now Unwrap() []error
Expand Down
2 changes: 1 addition & 1 deletion structured.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (se structuredErr) Format(s fmt.State, verb rune) {

// Slog creates an error that instead of generating a format string generates a structured slog Record.
// Accepts as args any valid slog args.
// Also accepts []slog.Attr as a single argument to avoid having to cast that argument.
// Also accepts `[]slog.Attr` as a single argument to avoid having to cast that argument.
// The slog Record can be retrieved with SlogRecord.
// Structured errors are more often created by wrapping existing errors with Wraps.
func Slog(msg string, args ...interface{}) StructuredError {
Expand Down

0 comments on commit 369f7a5

Please sign in to comment.