1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
|
// File: inspect.go
// Updated to support both error and *Error with delegation for cleaner *Error handling
package errors
import (
stderrs "errors"
"fmt"
"strings"
"time"
)
// Inspect provides detailed examination of an error, handling both single errors and MultiError
func Inspect(err error) {
if err == nil {
fmt.Println("No error occurred")
return
}
fmt.Printf("\n=== Error Inspection ===\n")
fmt.Printf("Top-level error: %v\n", err)
fmt.Printf("Top-level error type: %T\n", err)
// Handle *Error directly
if e, ok := err.(*Error); ok {
InspectError(e)
return
}
// Handle MultiError
if multi, ok := err.(*MultiError); ok {
allErrors := multi.Errors()
fmt.Printf("\nContains %d errors:\n", len(allErrors))
for i, e := range allErrors {
fmt.Printf("\n--- Error %d ---\n", i+1)
inspectSingleError(e)
}
} else {
// Inspect single error if not MultiError or *Error
fmt.Println("\n--- Details ---")
inspectSingleError(err)
}
// Additional diagnostics
fmt.Println("\n--- Diagnostics ---")
if IsRetryable(err) {
fmt.Println("- Error chain contains retryable errors")
}
if IsTimeout(err) {
fmt.Println("- Error chain contains timeout errors")
}
if code := getErrorCode(err); code != 0 {
fmt.Printf("- Highest priority error code: %d\n", code)
}
fmt.Printf("========================\n\n")
}
// InspectError provides detailed inspection of a specific *Error instance
func InspectError(err *Error) {
if err == nil {
fmt.Println("No error occurred")
return
}
fmt.Printf("\n=== Error Inspection (*Error) ===\n")
fmt.Printf("Top-level error: %v\n", err)
fmt.Printf("Top-level error type: %T\n", err)
fmt.Println("\n--- Details ---")
inspectSingleError(err) // Delegate to handle unwrapping and details
// Additional diagnostics specific to *Error
fmt.Println("\n--- Diagnostics ---")
if IsRetryable(err) {
fmt.Println("- Error is retryable")
}
if IsTimeout(err) {
fmt.Println("- Error chain contains timeout errors")
}
if code := err.Code(); code != 0 {
fmt.Printf("- Error code: %d\n", code)
}
fmt.Printf("========================\n\n")
}
// inspectSingleError handles inspection of a single error (may be part of a chain)
func inspectSingleError(err error) {
if err == nil {
fmt.Println(" (nil error)")
return
}
fmt.Printf(" Error: %v\n", err)
fmt.Printf(" Type: %T\n", err)
// Handle wrapped errors, including *Error type
var currentErr error = err
depth := 0
for currentErr != nil {
prefix := strings.Repeat(" ", depth+1)
if depth > 0 {
fmt.Printf("%sWrapped Cause (%T): %v\n", prefix, currentErr, currentErr)
}
// Check if it's our specific *Error type
if e, ok := currentErr.(*Error); ok {
if name := e.Name(); name != "" {
fmt.Printf("%sName: %s\n", prefix, name)
}
if cat := e.Category(); cat != "" {
fmt.Printf("%sCategory: %s\n", prefix, cat)
}
if code := e.Code(); code != 0 {
fmt.Printf("%sCode: %d\n", prefix, code)
}
if ctx := e.Context(); len(ctx) > 0 {
fmt.Printf("%sContext:\n", prefix)
for k, v := range ctx {
fmt.Printf("%s %s: %v\n", prefix, k, v)
}
}
if stack := e.Stack(); len(stack) > 0 {
fmt.Printf("%sStack (Top 3):\n", prefix)
limit := 3
if len(stack) < limit {
limit = len(stack)
}
for i := 0; i < limit; i++ {
fmt.Printf("%s %s\n", prefix, stack[i])
}
if len(stack) > limit {
fmt.Printf("%s ... (%d more frames)\n", prefix, len(stack)-limit)
}
}
}
// Unwrap using standard errors.Unwrap and handle *Error Unwrap
var nextErr error
// Prioritize *Error's Unwrap if available AND it returns non-nil
if e, ok := currentErr.(*Error); ok {
unwrapped := e.Unwrap()
if unwrapped != nil {
nextErr = unwrapped
} else {
// If *Error.Unwrap returns nil, fall back to standard unwrap
// This handles cases where *Error might wrap a non-standard error
// or where its internal cause is deliberately nil.
nextErr = stderrs.Unwrap(currentErr)
}
} else {
nextErr = stderrs.Unwrap(currentErr) // Fall back to standard unwrap for non-*Error types
}
// Prevent infinite loops if Unwrap returns the same error, or stop if no more unwrapping
if nextErr == currentErr || nextErr == nil {
break
}
currentErr = nextErr
depth++
if depth > 10 { // Safety break for very deep or potentially cyclic chains
fmt.Printf("%s... (chain too deep or potential cycle)\n", strings.Repeat(" ", depth+1))
break
}
}
}
// getErrorCode traverses the error chain to find the highest priority code.
// It uses errors.As to find the first *Error in the chain.
func getErrorCode(err error) int {
var code int = 0 // Default code
var target *Error
if As(err, &target) { // Use the package's As helper
if target != nil { // Add nil check for safety
code = target.Code()
}
}
// If the top-level error is *Error and has a code, it might take precedence.
// This depends on desired logic. Let's keep it simple for now: first code found by As.
if code == 0 { // Only check top-level if As didn't find one with a code
if e, ok := err.(*Error); ok {
code = e.Code()
}
}
return code
}
// handleError demonstrates using Inspect with additional handling logic
func handleError(err error) {
fmt.Println("\n=== Processing Failure ===")
Inspect(err) // Use the primary Inspect function
// Additional handling based on inspection
code := getErrorCode(err) // Use the helper
switch {
case IsTimeout(err):
fmt.Println("\nAction: Check connectivity or increase timeout")
case code == 402: // Check code obtained via helper
fmt.Println("\nAction: Payment processing failed - notify billing")
default:
fmt.Println("\nAction: Generic failure handling")
}
}
// processOrder demonstrates Chain usage with Inspect
func processOrder() error {
validateInput := func() error { return nil }
processPayment := func() error { return stderrs.New("credit card declined") }
sendNotification := func() error { fmt.Println("Notification sent."); return nil }
logOrder := func() error { fmt.Println("Order logged."); return nil }
chain := NewChain(ChainWithTimeout(2*time.Second)).
Step(validateInput).Tag("validation").
Step(processPayment).Tag("billing").Code(402).Retry(3, 100*time.Millisecond, WithRetryIf(IsRetryable)).
Step(sendNotification).Optional().
Step(logOrder)
err := chain.Run()
if err != nil {
handleError(err) // Call the unified error handler
return err // Propagate the error if needed
}
fmt.Println("Order processed successfully!")
return nil
}
|