errorsbp

package
v0.10.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 27, 2025 License: BSD-3-Clause Imports: 3 Imported by: 0

Documentation

Overview

Package errorsbp provides some error utilities for Baseplate.go project.

Batch

Batch can be used to compile multiple errors into a single one.

An example of how to use it in your functions:

type worker func() error

func runWorksParallel(works []worker) error {
    errChan := make(chan error, len(works))
    var wg sync.WaitGroup
    wg.Add(len(works))

    for _, work := range works {
        go func(work worker) {
            defer wg.Done()
            errChan <- work()
        }(work)
    }

    wg.Wait()
    close(errChan)
    var batch errorsbp.Batch
    for err := range errChan {
        // nil errors will be auto skipped
        batch.Add(err)
    }
    // If all works succeeded, Compile() returns nil.
    // If only one work failed, Compile() returns that error directly
    // instead of wrapping it inside BatchError.
    return batch.Compile()
}

Batch is not thread-safe. The same batch should not be operated on different goroutines concurrently.

Suppressor

Suppressor is a type defined to provide an unified way to allow certain functions/features to ignore certain errors.

It's currently used by thriftbp package in both server and client middlewares, to not treat certain errors defined in thrift IDL as span errors. Because of go's type system, we cannot reliably provide a Suppressor implementation to suppress all errors defined in all thrift IDLs, as a result we rely on service/client implementations to implement it for the middlewares.

An example of how to implement it for your thrift defined errors:

func MyThriftSuppressor(err error) bool {
    return errors.As(err, new(*mythrift.MyThriftErrorType))
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func BatchSize added in v0.9.2

func BatchSize(err error) int

BatchSize returns the size of the batch for error err.

If err implements `Unwrap() []error` (optional interface defined in go 1.20), or it unwraps to an error that implements `Unwrap() []error` (which includes errorsbp.Batch and *errorsbp.Batch), BatchSize returns the total size of Unwrap'd errors recursively. Otherwise, it returns 1 if err is non-nil, and 0 if err is nil.

Note that for a Batch, it's possible that BatchSize returns a different (higher) size than its Len. That would only happen if the Batch contains batch errors generated by other implementation(s) (for example, errors.Join or fmt.Errorf).

It's useful in tests, for example to verify that a function indeed returns the exact number of errors as expected.

It's possible to construct an error to cause BatchSize to recurse infinitely and thus cause stack overflow, so in general BatchSize should only be used in test code and not in production code.

Example

This example demonstrates how to use errorsbp.BatchSize in a unit test.

package main

import (
	"errors"
	"fmt"
	"testing"

	"github.com/reddit/baseplate.go/errorsbp"
)

var (
	errOne   = errors.New("dummy error #1")
	errTwo   = errors.New("dummy error #2")
	errThree = errors.New("dummy error #3")
)

// MyFunction is the function to be tested by MyFunctionTest.
func MyFunction(i int) error {
	var be errorsbp.Batch
	switch i {
	case 0:
		// do nothing
	case 1:
		be.Add(errOne)
	case 2:
		be.Add(errOne)
		be.Add(errTwo)
	case 3:
		be.Add(errOne)
		be.Add(errTwo)
		be.Add(errThree)
	}
	return be.Compile()
}

// NOTE: In real unit test code this function signature should be:
//
//	func TestMyFunction(t *testing.T)
//
// But doing that will break this example.
func MyFunctionTest() {
	var (
		t *testing.T
	)
	for _, c := range []struct {
		arg  int
		want []error
	}{
		{
			arg: 0,
		},
		{
			arg:  1,
			want: []error{errOne},
		},
		{
			arg:  2,
			want: []error{errOne, errTwo},
		},
		{
			arg:  3,
			want: []error{errOne, errTwo, errThree},
		},
	} {
		t.Run(fmt.Sprintf("%v", c.arg), func(t *testing.T) {
			got := MyFunction(c.arg)
			t.Logf("got error: %v", got)
			if len(c.want) != errorsbp.BatchSize(got) {
				t.Errorf("Expected %d errors, got %d", len(c.want), errorsbp.BatchSize(got))
			}
			for _, target := range c.want {
				if !errors.Is(got, target) {
					t.Errorf("Expected error %v to be returned but it's not", target)
				}
			}
		})
	}
}

// This example demonstrates how to use errorsbp.BatchSize in a unit test.
func main() {
	// See MyFuncionTest above for the real example.
}

func Prefix added in v0.9.12

func Prefix(prefix string, err error) error

Prefix is a helper function to add prefix to a potential error.

If err is nil, it returns nil. If prefix is empty string, it returns err as-is. Otherwise it returns an error that can unwrap to err with message of "prefix: err.Error()".

It's useful to be used with errors.Join.

func SuppressNone

func SuppressNone(err error) bool

SuppressNone is a Suppressor implementation that always return false, thus suppress none of the errors.

It's the default implementation nil Suppressor falls back into.

Types

type Batch deprecated

type Batch struct {
	// contains filtered or unexported fields
}

Batch is an error that can contain multiple errors.

The zero value of Batch is valid (with no errors) and ready to use.

This type is not thread-safe. The same batch should not be operated on different goroutines concurrently.

Deprecated: Please use errors.Join or fmt.Errorf with multiple %w instead.

Example (IncrementalErrors)

This example demonstrates how adding errors into a batch affects the single error returned by Compile().

package main

import (
	"errors"
	"fmt"

	"github.com/reddit/baseplate.go/errorsbp"
)

func main() {
	var batch errorsbp.Batch
	var singleError error

	singleError = batch.Compile()
	fmt.Printf("0: %v\n", singleError)

	err := errors.New("foo")
	batch.AddPrefix("prefix", err)
	singleError = batch.Compile()
	fmt.Printf("1: %v\n", singleError)

	batch.Add(nil)
	singleError = batch.Compile()
	fmt.Printf("Nil errors are skipped: %v\n", singleError)

	err = errors.New("bar")
	batch.Add(err)
	singleError = batch.Compile()
	fmt.Printf("2: %v\n", singleError)

	var newBatch errorsbp.Batch
	// Add multiple errors at once
	newBatch.Add(
		errors.New("fizz"),
		errors.New("buzz"),
	)
	newBatch.Add(batch)
	fmt.Printf("4: %v\n", newBatch.Compile())

}
Output:

0: <nil>
1: prefix: foo
Nil errors are skipped: prefix: foo
2: errorsbp.Batch: total 2 error(s) in this batch: prefix: foo; bar
4: errorsbp.Batch: total 4 error(s) in this batch: fizz; buzz; prefix: foo; bar

func (*Batch) Add

func (be *Batch) Add(errs ...error)

Add adds errors into the batch.

If an error is also a Batch, its underlying error(s) will be added instead of the Batch itself.

Nil errors will be skipped.

func (*Batch) AddPrefix added in v0.6.1

func (be *Batch) AddPrefix(prefix string, errs ...error)

AddPrefix adds errors into the batch with given prefix.

If an error is also a Batch, its underlying error(s) will be added instead of the Batch itself, all with the same given prefix.

Nil errors will be skipped.

The actual error(s) added into the batch will produce the error message of:

"prefix: err.Error()"

It's useful in Closer implementations that need to call multiple Closers.

Example

This example demonstrates how to use AddPrefix.

package main

import (
	"errors"
	"fmt"

	"github.com/reddit/baseplate.go/errorsbp"
)

func main() {
	operator1Failure := errors.New("unknown error")
	operator2Failure := errors.New("permission denied")
	operator0 := func() error {
		return nil
	}
	operator1 := func() error {
		return operator1Failure
	}
	operator2 := func() error {
		return operator2Failure
	}

	var batch errorsbp.Batch
	batch.AddPrefix("operator0", operator0())
	batch.AddPrefix("operator1", operator1())
	batch.AddPrefix("operator2", operator2())

	err := batch.Compile()
	fmt.Println(err)
	fmt.Println(errors.Is(err, operator1Failure))
	fmt.Println(errors.Is(err, operator2Failure))

}
Output:

errorsbp.Batch: total 2 error(s) in this batch: operator1: unknown error; operator2: permission denied
true
true

func (Batch) As

func (be Batch) As(v interface{}) bool

As implements helper interface for errors.As.

If v is pointer to either Batch or *Batch, *v will be set into this error. Otherwise, As will try errors.As against all errors in this batch, returning the first match.

See Is for the discussion of possiblity of infinite loop.

Example

This example demonstrates how a BatchError can be inspected with errors.As.

package main

import (
	"context"
	"errors"
	"fmt"
	"os"

	"github.com/reddit/baseplate.go/errorsbp"
)

func main() {
	var batch errorsbp.Batch
	var target *os.PathError

	batch.Add(context.Canceled)
	err := batch.Compile()
	fmt.Println(errors.As(err, &target)) // false

	batch.Add(fmt.Errorf("wrapped: %w", &os.PathError{}))
	err = batch.Compile()
	fmt.Println(errors.As(err, &target)) // true

	batch.Add(fmt.Errorf("wrapped: %w", &os.LinkError{}))
	err = batch.Compile()
	fmt.Println(errors.As(err, &target)) // true

}
Output:

false
true
true

func (*Batch) Clear

func (be *Batch) Clear()

Clear clears the batch.

func (Batch) Compile

func (be Batch) Compile() error

Compile compiles the batch.

If the batch contains zero errors, Compile returns nil.

If the batch contains exactly one error, that underlying error will be returned.

Otherwise, the batch itself will be returned.

func (Batch) Error

func (be Batch) Error() string

func (Batch) GetErrors

func (be Batch) GetErrors() []error

GetErrors returns a copy of the underlying error(s).

func (Batch) Is

func (be Batch) Is(target error) bool

Is implements helper interface for errors.Is.

It calls errors.Is against all errors in this batch, until a match is found.

If an error in the batch is the Batch itself, calling its Is (and As) could cause an infinite loop. But there's a special handling in Add function, that if you try to add a Batch into the batch, we add the underlying errors instead the Batch itself. As a result it should be impossible to cause infinite loops in Is and As.

Example

This example demonstrates how a BatchError can be inspected with errors.Is.

package main

import (
	"context"
	"errors"
	"fmt"
	"io"

	"github.com/reddit/baseplate.go/errorsbp"
)

func main() {
	var batch errorsbp.Batch

	batch.Add(context.Canceled)
	err := batch.Compile()
	fmt.Println(errors.Is(err, context.Canceled)) // true
	fmt.Println(errors.Is(err, io.EOF))           // false

	batch.Add(fmt.Errorf("wrapped: %w", io.EOF))
	err = batch.Compile()
	fmt.Println(errors.Is(err, context.Canceled)) // true
	fmt.Println(errors.Is(err, io.EOF))           // true

}
Output:

true
false
true
true

func (Batch) Len added in v0.9.2

func (be Batch) Len() int

Len returns the size of the batch.

Note that this is the naive size without traversal recursively. See BatchSize for the accumulated size with recursive traversal instead.

func (Batch) Unwrap added in v0.9.12

func (be Batch) Unwrap() []error

Unwrap implements the optional interface defined in go 1.20.

It's an alias to GetErrors.

type Suppressor

type Suppressor func(err error) bool

Suppressor defines a type of function can be used to suppress certain errors.

The implementation shall return true on the errors they want to suppress, and false on all other errors.

Example

This example demonstrates how to implement a Suppressor.

package main

import (
	"errors"

	"github.com/reddit/baseplate.go/errorsbp"
	"github.com/reddit/baseplate.go/internal/gen-go/reddit/baseplate"
)

// BEGIN THRIFT GENERATED CODE
//
// In real code this part should come from thrift compiled go code.
// Here we just write some placeholders to use as an example.

type MyThriftException struct{}

func (*MyThriftException) Error() string {
	return "my thrift exception"
}

// END THRIFT GENERATED CODE

func MyThriftExceptionSuppressor(err error) bool {
	return errors.As(err, new(*MyThriftException))
}

func BaseplateErrorSuppressor(err error) bool {
	// baseplate.Error is from a thrift exception defined in baspleate.thrift,
	// then compiled to go code by thrift compiler.
	// We use that type as an example here.
	// In real code you usually also should add (Or) additional exceptions defined
	// in your thrift files.
	return errors.As(err, new(*baseplate.Error))
}

// This example demonstrates how to implement a Suppressor.
func main() {
	// This constructs the Suppressor you could fill into
	// thriftbp.ServerConfig.ErrorSpanSuppressor field.
	errorsbp.OrSuppressors(BaseplateErrorSuppressor, MyThriftExceptionSuppressor)
}

func OrSuppressors

func OrSuppressors(suppressors ...Suppressor) Suppressor

OrSuppressors combines the given suppressors.

If any of the suppressors return true on an error, the combined Suppressor would returns true on that error.

func (Suppressor) Suppress

func (s Suppressor) Suppress(err error) bool

Suppress is the nil-safe way of calling a Suppressor.

func (Suppressor) Wrap

func (s Suppressor) Wrap(err error) error

Wrap wraps the error based on the decision of Suppressor.

If the error shall be suppressed, Wrap returns nil. Otherwise Wrap returns the error as-is.

Like Suppress, Wrap is also nil-safe.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL