package error import ( "encoding/json" "errors" "fmt" "strings" ) type ( NestedError = *NestedErrorImpl NestedErrorImpl struct { subject string err error extras []NestedErrorImpl } JSONNestedError struct { Subject string `json:"subject"` Err string `json:"error"` Extras []JSONNestedError `json:"extras,omitempty"` } ) func From(err error) NestedError { if IsNil(err) { return nil } return &NestedErrorImpl{err: err} } func FromJSON(data []byte) (NestedError, bool) { var j JSONNestedError if err := json.Unmarshal(data, &j); err != nil { return nil, false } if j.Err == "" { return nil, false } extras := make([]NestedErrorImpl, len(j.Extras)) for i, e := range j.Extras { extra, ok := fromJSONObject(e) if !ok { return nil, false } extras[i] = *extra } return &NestedErrorImpl{ subject: j.Subject, err: errors.New(j.Err), extras: extras, }, true } // Check is a helper function that // convert (T, error) to (T, NestedError). func Check[T any](obj T, err error) (T, NestedError) { return obj, From(err) } func Join(message string, err ...NestedError) NestedError { extras := make([]NestedErrorImpl, len(err)) nErr := 0 for i, e := range err { if e == nil { continue } extras[i] = *e nErr++ } if nErr == 0 { return nil } return &NestedErrorImpl{ err: errors.New(message), extras: extras, } } func JoinE(message string, err ...error) NestedError { b := NewBuilder("%s", message) for _, e := range err { b.AddE(e) } return b.Build() } func IsNil(err error) bool { return err == nil } func IsNotNil(err error) bool { return err != nil } func (ne NestedError) String() string { var buf strings.Builder ne.writeToSB(&buf, 0, "") return buf.String() } func (ne NestedError) Is(err error) bool { if ne == nil { return err == nil } // return errors.Is(ne.err, err) if errors.Is(ne.err, err) { return true } for _, e := range ne.extras { if e.Is(err) { return true } } return false } func (ne NestedError) IsNot(err error) bool { return !ne.Is(err) } func (ne NestedError) Error() error { if ne == nil { return nil } return ne.buildError(0, "") } func (ne NestedError) With(s any) NestedError { if ne == nil { return ne } var msg string switch ss := s.(type) { case nil: return ne case NestedError: return ne.withError(ss) case error: return ne.withError(From(ss)) case string: msg = ss case fmt.Stringer: return ne.appendMsg(ss.String()) default: return ne.appendMsg(fmt.Sprint(s)) } return ne.withError(From(errors.New(msg))) } func (ne NestedError) Extraf(format string, args ...any) NestedError { return ne.With(errorf(format, args...)) } func (ne NestedError) Subject(s any, sep ...string) NestedError { if ne == nil { return ne } var subject string switch ss := s.(type) { case string: subject = ss case fmt.Stringer: subject = ss.String() default: subject = fmt.Sprint(s) } switch { case ne.subject == "": ne.subject = subject case len(sep) > 0: ne.subject = fmt.Sprintf("%s%s%s", subject, sep[0], ne.subject) default: ne.subject = fmt.Sprintf("%s > %s", subject, ne.subject) } return ne } func (ne NestedError) Subjectf(format string, args ...any) NestedError { if ne == nil { return ne } return ne.Subject(fmt.Sprintf(format, args...)) } func (ne NestedError) JSONObject() JSONNestedError { extras := make([]JSONNestedError, len(ne.extras)) for i, e := range ne.extras { extras[i] = e.JSONObject() } return JSONNestedError{ Subject: ne.subject, Err: ne.err.Error(), Extras: extras, } } func (ne NestedError) JSON() []byte { b, err := json.MarshalIndent(ne.JSONObject(), "", " ") if err != nil { panic(err) } return b } func (ne NestedError) NoError() bool { return ne == nil } func (ne NestedError) HasError() bool { return ne != nil } func errorf(format string, args ...any) NestedError { return From(fmt.Errorf(format, args...)) } func fromJSONObject(obj JSONNestedError) (NestedError, bool) { data, err := json.Marshal(obj) if err != nil { return nil, false } return FromJSON(data) } func (ne NestedError) withError(err NestedError) NestedError { if ne != nil && err != nil { ne.extras = append(ne.extras, *err) } return ne } func (ne NestedError) appendMsg(msg string) NestedError { if ne == nil { return nil } ne.err = fmt.Errorf("%w %s", ne.err, msg) return ne } func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) { for range level { sb.WriteString(" ") } sb.WriteString(prefix) if ne.NoError() { sb.WriteString("nil") return } sb.WriteString(ne.err.Error()) if ne.subject != "" { sb.WriteString(fmt.Sprintf(" for %q", ne.subject)) } if len(ne.extras) > 0 { sb.WriteRune(':') for _, extra := range ne.extras { sb.WriteRune('\n') extra.writeToSB(sb, level+1, "- ") } } } func (ne NestedError) buildError(level int, prefix string) error { var res error var sb strings.Builder for range level { sb.WriteString(" ") } sb.WriteString(prefix) if ne.NoError() { sb.WriteString("nil") return errors.New(sb.String()) } res = fmt.Errorf("%s%w", sb.String(), ne.err) sb.Reset() if ne.subject != "" { sb.WriteString(fmt.Sprintf(" for %q", ne.subject)) } if len(ne.extras) > 0 { sb.WriteRune(':') res = fmt.Errorf("%w%s", res, sb.String()) for _, extra := range ne.extras { res = errors.Join(res, extra.buildError(level+1, "- ")) } } else { res = fmt.Errorf("%w%s", res, sb.String()) } return res }