Skip to content

proposal: errors: add Errors as a standard way to represent multiple errors as a single error #47811

Closed
@marksalpeter

Description

@marksalpeter

Description

It is common practice in golang to return an error from a func in the event of a problem during its execution (e.g. func myFunc() error { ... }).

However, there are a few cases, such as field validation on a struct, where it is useful to return multiple errors from a function (e.g. func validate(m *Model) []error). Sometimes these errors need to be 'bubbled up' through a call stack that only returns one error. So, I find myself having to re-implement custom errors that represent many errors in each of my projects.

I think a generic solution could be added to the standard libraries' errors package that serves this purpose.

Example Implementation

I've updated the example implementation to incorporate the feedback from @neild and @D1CED

package util

import (
	"fmt"
)

// Errors are many errors that can be represented as a single error
type Errors interface {
	Errors() []error
	error
}

// NewErrors combine many errors into a single error
func NewErrors(errs ...error) error {
	if lenErrs := len(errs); lenErrs == 0 {
		return nil
	} else if lenErrs == 1 {
		return errs[0]
	}
	var es manyErrors
	for _, err := range errs {
		// Merge many slices of errors into a single slice
		if errs, ok := err.(Errors); ok {
			es = append(es, errs.Errors()...)
			continue
		}
		es = append(es, err)
	}
	return es
}

// manyErrors combines many errors into a single error
type manyErrors []error

// Is works with errors.Is
func (es errors) Is(find error) bool {
        for _, e := range es {
                if Is(e, find) {
                        return true
                }
        }
        return false
}       

// As works with errors.As
func (es errors) As(find interface{}) bool {
        for _, e := range es {
                if As(e, find) {
                        return true
                }
        }
        return false
}

// Errors implements the Errors interface
func (es manyErrors) Errors() []error {
	return []error(es)
}

// Error implements the error interface
func (es manyErrors) Error() string {
	return fmt.Sprintf("[%v", []error(es))
}

Example Usage

import "errors"

type Model struct {
	FieldOne   string `json:"field_one"`
	FieldTwo   string `json:"field_two"`
	FieldThree string `json:"field_three"`
}

func (m *Model) Validate() error {
	var errs []error
	if len(m.FieldOne) < 1 {
		errs = append(errs, errors.New("'field_one' is required"))
	}
	if len(m.FieldTwo) < 1 {
		errs = append(errs, errors.New("'field_two' is required"))
	}
	if len(m.FieldThree) < 1 {
		errs = append(errs, errors.New("'field_three' is required"))
	}
	return errors.NewErrors(errs...)
}

Conclusion

Whether or not this proposal is accepted, I'm curious how others have approached this problem in the past and if there's already a commonly referenced solution that I'm unfamiliar with. Thanks 🙏

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions