Skip to content

Commit 893fb18

Browse files
author
F.
committed
{feat) docs
1 parent 390e407 commit 893fb18

File tree

2 files changed

+502
-0
lines changed

2 files changed

+502
-0
lines changed

docs/docs/features/error-wrapping.md

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# Error Wrapping
2+
3+
Error wrapping is a powerful feature that allows you to add context to errors as they propagate through your application. Understanding how to effectively wrap errors can significantly improve your application's debuggability and error handling capabilities.
4+
5+
## Understanding Error Wrapping
6+
7+
When an error occurs deep in your application's call stack, it often needs to pass through several layers before being handled. Each layer might need to add its own context to the error, helping to tell the complete story of what went wrong.
8+
9+
Consider this scenario:
10+
11+
```go
12+
func getUserProfile(userID string) (*Profile, error) {
13+
// Low level database error occurs
14+
data, err := db.Query("SELECT * FROM users WHERE id = ?", userID)
15+
if err != nil {
16+
// We wrap the database error with our context
17+
return nil, ewrap.Wrap(err, "failed to fetch user data",
18+
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityError))
19+
}
20+
21+
// Error occurs during data processing
22+
profile, err := parseUserData(data)
23+
if err != nil {
24+
// We wrap the parsing error with additional context
25+
return nil, ewrap.Wrap(err, "failed to parse user profile",
26+
ewrap.WithContext(ctx, ewrap.ErrorTypeInternal, ewrap.SeverityError)).
27+
WithMetadata("user_id", userID)
28+
}
29+
30+
return profile, nil
31+
}
32+
```
33+
34+
## The Wrap Function
35+
36+
The `Wrap` function is the primary tool for error wrapping. It preserves the original error while adding new context:
37+
38+
```go
39+
func Wrap(err error, msg string, opts ...Option) *Error
40+
```
41+
42+
The function takes:
43+
44+
- The original error
45+
- A message describing what went wrong at this level
46+
- Optional configuration options
47+
48+
### Basic Usage
49+
50+
Here's a simple example of error wrapping:
51+
52+
```go
53+
if err := validateInput(data); err != nil {
54+
return ewrap.Wrap(err, "input validation failed")
55+
}
56+
```
57+
58+
### Adding Context While Wrapping
59+
60+
You can add rich context while wrapping errors:
61+
62+
```go
63+
if err := processPayment(amount); err != nil {
64+
return ewrap.Wrap(err, "payment processing failed",
65+
ewrap.WithContext(ctx, ewrap.ErrorTypeExternal, ewrap.SeverityCritical),
66+
ewrap.WithLogger(logger)).
67+
WithMetadata("amount", amount).
68+
WithMetadata("currency", "USD").
69+
WithMetadata("processor", "stripe")
70+
}
71+
```
72+
73+
## Error Chain Preservation
74+
75+
When you wrap an error, ewrap maintains the entire error chain. This means you can:
76+
77+
- Access the original error
78+
- See all intermediate wrapping contexts
79+
- Understand the complete error path
80+
81+
```go
82+
func main() {
83+
err := processUserRequest()
84+
if err != nil {
85+
// Print the full error chain
86+
fmt.Println(err)
87+
88+
// Access the root cause
89+
cause := errors.Unwrap(err)
90+
91+
// Check if a specific error type exists in the chain
92+
if errors.Is(err, sql.ErrNoRows) {
93+
// Handle database not found case
94+
}
95+
}
96+
}
97+
```
98+
99+
## Formatted Wrapping with Wrapf
100+
101+
For cases where you need to include formatted messages, use `Wrapf`:
102+
103+
```go
104+
func updateUser(userID string, fields map[string]interface{}) error {
105+
if err := db.Update(userID, fields); err != nil {
106+
return ewrap.Wrapf(err, "failed to update user %s", userID)
107+
}
108+
return nil
109+
}
110+
```
111+
112+
## Best Practices for Error Wrapping
113+
114+
### 1. Add Meaningful Context
115+
116+
Each wrap should add valuable information:
117+
118+
```go
119+
// Good - adds specific context
120+
err = ewrap.Wrap(err, "failed to process monthly report for January 2024",
121+
ewrap.WithMetadata("report_type", "monthly"),
122+
ewrap.WithMetadata("period", "2024-01"))
123+
124+
// Not as helpful - too generic
125+
err = ewrap.Wrap(err, "processing failed")
126+
```
127+
128+
### 2. Preserve Error Types
129+
130+
Choose error types that make sense for the current context:
131+
132+
```go
133+
func validateAndSaveUser(user User) error {
134+
err := validateUser(user)
135+
if err != nil {
136+
// Preserve validation error type
137+
return ewrap.Wrap(err, "user validation failed",
138+
ewrap.WithContext(ctx, ewrap.ErrorTypeValidation, ewrap.SeverityError))
139+
}
140+
141+
err = saveUser(user)
142+
if err != nil {
143+
// Use database error type for storage issues
144+
return ewrap.Wrap(err, "failed to save user",
145+
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityError))
146+
}
147+
148+
return nil
149+
}
150+
```
151+
152+
### 3. Use Appropriate Granularity
153+
154+
Balance between too much and too little information:
155+
156+
```go
157+
func processOrder(order Order) error {
158+
// Wrap high-level business operations
159+
if err := validateOrder(order); err != nil {
160+
return ewrap.Wrap(err, "order validation failed")
161+
}
162+
163+
// Don't wrap every small utility function
164+
total := calculateTotal(order.Items)
165+
166+
// Wrap significant state transitions or external calls
167+
if err := chargeCustomer(order.CustomerID, total); err != nil {
168+
return ewrap.Wrap(err, "payment processing failed",
169+
ewrap.WithMetadata("amount", total),
170+
ewrap.WithMetadata("customer_id", order.CustomerID))
171+
}
172+
173+
return nil
174+
}
175+
```
176+
177+
### 4. Consider Performance
178+
179+
While error wrapping is lightweight, be mindful in hot paths:
180+
181+
```go
182+
func processItems(items []Item) error {
183+
for _, item := range items {
184+
// In tight loops, consider if wrapping is necessary
185+
if err := validateItem(item); err != nil {
186+
return err // Maybe don't wrap simple validation errors
187+
}
188+
189+
// Do wrap significant errors
190+
if err := processItem(item); err != nil {
191+
return ewrap.Wrap(err, "item processing failed",
192+
ewrap.WithMetadata("item_id", item.ID))
193+
}
194+
}
195+
return nil
196+
}
197+
```
198+
199+
## Advanced Error Wrapping
200+
201+
### Conditional Wrapping
202+
203+
Sometimes you might want to wrap errors differently based on their type:
204+
205+
```go
206+
func handleDatabaseOperation() error {
207+
err := db.Query()
208+
if err != nil {
209+
switch {
210+
case errors.Is(err, sql.ErrNoRows):
211+
return ewrap.Wrap(err, "record not found",
212+
ewrap.WithContext(ctx, ewrap.ErrorTypeNotFound, ewrap.SeverityWarning))
213+
case errors.Is(err, sql.ErrConnDone):
214+
return ewrap.Wrap(err, "database connection lost",
215+
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityCritical))
216+
default:
217+
return ewrap.Wrap(err, "database operation failed",
218+
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityError))
219+
}
220+
}
221+
return nil
222+
}
223+
```
224+
225+
### Multi-Level Wrapping
226+
227+
For complex operations, you might wrap errors multiple times:
228+
229+
```go
230+
func processUserOrder(ctx context.Context, userID, orderID string) error {
231+
user, err := getUser(userID)
232+
if err != nil {
233+
return ewrap.Wrap(err, "failed to get user",
234+
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityError))
235+
}
236+
237+
order, err := getOrder(orderID)
238+
if err != nil {
239+
return ewrap.Wrap(err, "failed to get order",
240+
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityError))
241+
}
242+
243+
if err := validateUserCanAccessOrder(user, order); err != nil {
244+
return ewrap.Wrap(err, "user not authorized to access order",
245+
ewrap.WithContext(ctx, ewrap.ErrorTypePermission, ewrap.SeverityWarning))
246+
}
247+
248+
if err := processOrderPayment(order); err != nil {
249+
return ewrap.Wrap(err, "order payment failed",
250+
ewrap.WithContext(ctx, ewrap.ErrorTypeExternal, ewrap.SeverityCritical))
251+
}
252+
253+
return nil
254+
}
255+
```

0 commit comments

Comments
 (0)