github.com/richardwilkes/toolbox@v1.121.0/errs/errors.go (about)

     1  // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the Mozilla Public
     4  // License, version 2.0. If a copy of the MPL was not distributed with
     5  // this file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6  //
     7  // This Source Code Form is "Incompatible With Secondary Licenses", as
     8  // defined by the Mozilla Public License, version 2.0.
     9  
    10  // Package errs implements a detailed error object that provides stack traces
    11  // with source locations, along with nested causes, if any.
    12  package errs
    13  
    14  import (
    15  	"errors"
    16  	"fmt"
    17  	"log/slog"
    18  	"os"
    19  	"reflect"
    20  	"runtime"
    21  	"strconv"
    22  	"strings"
    23  )
    24  
    25  var (
    26  	_ ErrorWrapper   = &Error{}
    27  	_ StackError     = &Error{}
    28  	_ fmt.Formatter  = &Error{}
    29  	_ slog.LogValuer = &Error{}
    30  
    31  	// RuntimePrefixesToFilter is a list of prefixes to filter out of the stack trace.
    32  	//
    33  	// This variable is not used in a thread-safe manner, so any alterations should be done before any goroutines are
    34  	// started.
    35  	RuntimePrefixesToFilter = []string{
    36  		"runtime.",
    37  		"testing.",
    38  		"github.com/richardwilkes/toolbox/errs.",
    39  	}
    40  )
    41  
    42  // ErrorWrapper contains methods for interacting with the wrapped errors.
    43  type ErrorWrapper interface {
    44  	error
    45  	Count() int
    46  	WrappedErrors() []error
    47  }
    48  
    49  // StackError contains methods with the stack trace and message.
    50  type StackError interface {
    51  	error
    52  	Message() string
    53  	Detail(trimRuntime bool) string
    54  	StackTrace(trimRuntime bool) string
    55  }
    56  
    57  // Error holds the detailed error message.
    58  type Error struct {
    59  	cause   error
    60  	next    *Error
    61  	message string
    62  	stack   []uintptr
    63  	wrapped bool
    64  }
    65  
    66  // CloneWithPrefixMessage clones this error and adds a prefix to its message.
    67  func (e *Error) CloneWithPrefixMessage(prefix string) error {
    68  	revised := *e
    69  	revised.message = prefix + revised.message
    70  	return &revised
    71  }
    72  
    73  // Wrap an error and turn it into a detailed error. If error is already a detailed error or nil, it will be returned
    74  // as-is.
    75  func Wrap(cause error) error {
    76  	if isNil(cause) {
    77  		return nil
    78  	}
    79  	var errorPtr *Error
    80  	if errors.As(cause, &errorPtr) {
    81  		return cause
    82  	}
    83  	return &Error{
    84  		message: cause.Error(),
    85  		stack:   callStack(),
    86  		cause:   cause,
    87  		wrapped: true,
    88  	}
    89  }
    90  
    91  // WrapTyped wraps an error and turns it into a detailed error. If error is already a detailed error or nil, it will be
    92  // returned as-is. This method returns the error as an *Error. Use Wrap() to receive a generic error.
    93  func WrapTyped(cause error) *Error {
    94  	if isNil(cause) {
    95  		return nil
    96  	}
    97  	// Intentionally not checking to see if there is a deeper wrapped *Error as the error must be wrapped again in order
    98  	// to avoid losing information and still return a *Error
    99  	//nolint:errorlint // See note above
   100  	if err, ok := cause.(*Error); ok {
   101  		return err
   102  	}
   103  	return &Error{
   104  		message: cause.Error(),
   105  		stack:   callStack(),
   106  		cause:   cause,
   107  		wrapped: true,
   108  	}
   109  }
   110  
   111  // New creates a new detailed error with the 'message'.
   112  func New(message string) *Error {
   113  	return &Error{
   114  		message: message,
   115  		stack:   callStack(),
   116  	}
   117  }
   118  
   119  // Newf creates a new detailed error using fmt.Sprintf() to format the message.
   120  func Newf(format string, v ...any) *Error {
   121  	return New(fmt.Sprintf(format, v...))
   122  }
   123  
   124  // NewWithCause creates a new detailed error with the 'message' and underlying 'cause'.
   125  func NewWithCause(message string, cause error) *Error {
   126  	return &Error{
   127  		message: message,
   128  		stack:   callStack(),
   129  		cause:   cause,
   130  	}
   131  }
   132  
   133  // NewWithCausef creates a new detailed error with an underlying 'cause' and using fmt.Sprintf() to format the message.
   134  func NewWithCausef(cause error, format string, v ...any) *Error {
   135  	return NewWithCause(fmt.Sprintf(format, v...), cause)
   136  }
   137  
   138  // Append one or more errors to an existing error. err may be nil.
   139  func Append(err error, errs ...error) *Error {
   140  	//nolint:errorlint // Explicitly only want to look at this exact error and not things wrapped inside it
   141  	switch e := err.(type) {
   142  	case *Error:
   143  		var root *Error
   144  		if !e.empty() {
   145  			root = e
   146  			for e.next != nil {
   147  				e = e.next
   148  			}
   149  		} else {
   150  			e = nil
   151  		}
   152  		for _, one := range errs {
   153  			var next *Error
   154  			//nolint:errorlint // Explicitly only want to look at this exact error and not things wrapped inside it
   155  			switch typedErr := one.(type) {
   156  			case *Error:
   157  				if !typedErr.empty() {
   158  					n := *typedErr
   159  					localRoot := &n
   160  					next = localRoot
   161  					for next.next != nil {
   162  						copied := *next.next
   163  						next.next = &copied
   164  						next = next.next
   165  					}
   166  					next = localRoot
   167  				}
   168  			default:
   169  				if typedErr != nil {
   170  					next = &Error{
   171  						message: typedErr.Error(),
   172  						stack:   callStack(),
   173  						cause:   typedErr,
   174  						wrapped: true,
   175  					}
   176  				}
   177  			}
   178  			if next != nil {
   179  				if e == nil {
   180  					root = next
   181  				} else {
   182  					e.next = next
   183  				}
   184  				e = next
   185  			}
   186  		}
   187  		return root
   188  	default:
   189  		if e == nil {
   190  			if len(errs) == 0 {
   191  				return nil
   192  			}
   193  			return Append(errs[0], errs[1:]...)
   194  		}
   195  		return Append(WrapTyped(e), errs...)
   196  	}
   197  }
   198  
   199  func callStack() []uintptr {
   200  	var pcs [512]uintptr
   201  	n := runtime.Callers(3, pcs[:])
   202  	cs := make([]uintptr, n)
   203  	copy(cs, pcs[:n])
   204  	return cs
   205  }
   206  
   207  // Count returns the number of contained errors, not including causes.
   208  func (e *Error) Count() int {
   209  	count := 0
   210  	err := e
   211  	for err != nil {
   212  		if !err.empty() {
   213  			count++
   214  		}
   215  		err = err.next
   216  	}
   217  	return count
   218  }
   219  
   220  // Message returns the message attached to this error.
   221  func (e *Error) Message() string {
   222  	if e.next == nil {
   223  		return e.message
   224  	}
   225  	var buffer strings.Builder
   226  	buffer.WriteString(fmt.Sprintf("Multiple (%d) errors occurred:", e.Count()))
   227  	err := e
   228  	for err != nil {
   229  		buffer.WriteString("\n- ")
   230  		buffer.WriteString(err.message)
   231  		err = err.next
   232  	}
   233  	return buffer.String()
   234  }
   235  
   236  // Error implements the error interface.
   237  func (e *Error) Error() string {
   238  	return e.Detail(true)
   239  }
   240  
   241  // Detail returns the fully detailed error message, which includes the primary message, the call stack, and potentially
   242  // one or more chained causes. Note that any included stack trace will be only for the first error in the case where
   243  // multiple errors were accumulated into one via calls to .Append().
   244  func (e *Error) Detail(trimRuntime bool) string {
   245  	msg := e.Message()
   246  	stack := e.StackTrace(trimRuntime)
   247  	switch {
   248  	case msg == "" && stack == "":
   249  		return "<no detail>"
   250  	case msg == "":
   251  		return stack
   252  	case stack == "":
   253  		return msg
   254  	default:
   255  		return msg + "\n" + stack
   256  	}
   257  }
   258  
   259  // StackTrace returns just the stack trace portion of the message.
   260  func (e *Error) StackTrace(trimRuntime bool) string {
   261  	var buffer strings.Builder
   262  	frames := runtime.CallersFrames(e.stack)
   263  	for {
   264  		frame, more := frames.Next()
   265  		if frame.Function != "" {
   266  			if trimRuntime {
   267  				if frame.Function == "main.main" && frame.File == "_testmain.go" {
   268  					continue
   269  				}
   270  				skip := false
   271  				for _, prefix := range RuntimePrefixesToFilter {
   272  					if strings.HasPrefix(frame.Function, prefix) {
   273  						skip = true
   274  						break
   275  					}
   276  				}
   277  				if skip {
   278  					continue
   279  				}
   280  			}
   281  			if buffer.Len() != 0 {
   282  				buffer.WriteByte('\n')
   283  			}
   284  			buffer.WriteString("    [")
   285  			buffer.WriteString(frame.Function)
   286  			buffer.WriteString("] ")
   287  			file := frame.File
   288  			if i := strings.Index(file, "."); i != -1 {
   289  				for i > 0 && file[i] != os.PathSeparator {
   290  					i--
   291  				}
   292  				if i > 0 {
   293  					file = file[i+1:]
   294  				}
   295  				if i = strings.LastIndexByte(file, os.PathSeparator); i != -1 {
   296  					path := file[:i]
   297  					offset := i + 1
   298  					if i = strings.LastIndexByte(path, os.PathSeparator); i != -1 {
   299  						if path[i+1:] == "_obj" {
   300  							path = path[:i]
   301  						}
   302  					}
   303  					if strings.HasPrefix(frame.Function, path) {
   304  						file = file[offset:]
   305  					}
   306  				}
   307  			}
   308  			buffer.WriteString(file)
   309  			buffer.WriteByte(':')
   310  			buffer.WriteString(strconv.Itoa(frame.Line))
   311  		}
   312  		if !more {
   313  			break
   314  		}
   315  	}
   316  	if e.cause != nil && !e.wrapped {
   317  		buffer.WriteString("\n  Caused by: ")
   318  		//nolint:errorlint // Explicitly only want to look at this exact error and not things wrapped inside it
   319  		if detailed, ok := e.cause.(*Error); ok {
   320  			buffer.WriteString(detailed.Detail(trimRuntime))
   321  		} else {
   322  			buffer.WriteString(e.cause.Error())
   323  		}
   324  	}
   325  	return buffer.String()
   326  }
   327  
   328  // RawStackTrace returns the raw call stack pointers for the first error within this error.
   329  func (e *Error) RawStackTrace() []uintptr {
   330  	return e.stack
   331  }
   332  
   333  // ErrorOrNil returns an error interface if this Error represents one or more errors, or nil if it is empty.
   334  func (e *Error) ErrorOrNil() error {
   335  	if e.empty() {
   336  		return nil
   337  	}
   338  	return e
   339  }
   340  
   341  func (e *Error) empty() bool {
   342  	return e == nil || (e.message == "" && e.stack == nil && e.cause == nil && e.next == nil)
   343  }
   344  
   345  // WrappedErrors returns the contained errors.
   346  func (e *Error) WrappedErrors() []error {
   347  	result := make([]error, 0, e.Count())
   348  	err := e
   349  	for err != nil {
   350  		eCopy := *err
   351  		eCopy.next = nil
   352  		result = append(result, &eCopy)
   353  		err = err.next
   354  	}
   355  	return result
   356  }
   357  
   358  // Unwrap implements errors.Unwrap and returns the underlying cause, if any.
   359  func (e *Error) Unwrap() error {
   360  	return e.cause
   361  }
   362  
   363  // Format implements the fmt.Formatter interface.
   364  //
   365  // Supported formats:
   366  //   - "%s"  Just the message
   367  //   - "%q"  Just the message, but quoted
   368  //   - "%v"  The message plus a stack trace, trimmed of golang runtime calls
   369  //   - "%+v" The message plus a stack trace
   370  func (e *Error) Format(state fmt.State, verb rune) {
   371  	switch verb {
   372  	case 'v':
   373  		_, _ = state.Write([]byte(e.Detail(!state.Flag('+'))))
   374  	case 's':
   375  		_, _ = state.Write([]byte(e.Message()))
   376  	case 'q':
   377  		_, _ = fmt.Fprintf(state, "%q", e.Message())
   378  	}
   379  }
   380  
   381  // LogValue implements the slog.LogValuer interface.
   382  func (e *Error) LogValue() slog.Value {
   383  	return slog.GroupValue(
   384  		slog.String(slog.MessageKey, e.Message()),
   385  		slog.Any(StackTraceKey, &stackValue{err: e}),
   386  	)
   387  }
   388  
   389  // Can't use toolbox.IsNil() due to circular dependencies, so put a copy of the code here, too.
   390  func isNil(i any) bool {
   391  	if i == nil {
   392  		return true
   393  	}
   394  	switch reflect.TypeOf(i).Kind() {
   395  	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer:
   396  		return reflect.ValueOf(i).IsNil()
   397  	}
   398  	return false
   399  }