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 }