- Always wrap errors when crossing package boundaries
- Include operation context (“unmarshal”, “dial”, “fetch”)
- Include relevant identifiers (userID, filename, URL)
- Don’t include sensitive data (passwords, tokens, secrets)
- Be consistent within your codebase
- Consider your audience (internal vs external errors)
- Use structured logging for detailed debugging info instead of cramming everything into error messages
// DO: Add context when wrapping errors
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("unmarshal config: %w", err)
}
// DON'T: Just pass through without context
if err := json.Unmarshal(data, &result); err != nil {
return err
}
When to Add Context:
- Add context when: The error location/cause isn't obvious from the call stack
- Skip context when: The error is already clear and you're just passing it up one level
- Always add context when: Crossing package boundaries
// ✅ SAFE: Include non-sensitive operational data
return fmt.Errorf("dial database: %w, host=%s, port=%d", err, host, port)
// ✅ SAFE: Include identifiers and operation context
return fmt.Errorf("fetch user: %w, userID=%d", err, userID)
// ✅ SAFE: Include file paths (usually)
return fmt.Errorf("read config file: %w, path=%s", err, configPath)
// ✅ SAFE: Include request/operation details
return fmt.Errorf("HTTP request failed: %w, method=%s, url=%s", err, method, url)
// ❌ UNSAFE: Don't include sensitive data
return fmt.Errorf("auth failed: %w, password=%s", err, password)
// ❌ UNSAFE: Don't include tokens/secrets
return fmt.Errorf("API call failed: %w, token=%s", err, apiToken)
// ❌ UNSAFE: Don't include internal system details in user-facing errors
return fmt.Errorf("query failed: %w, sql=%s", err, sqlQuery)
// Pattern: "operation: %w"
func loadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config file: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("unmarshal config: %w", err)
}
return nil
}
// Pattern: "operation failed: %w, context=value"
func dialDatabase(host string, port int) error {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return fmt.Errorf("dial database: %w, host=%s, port=%d", err, host, port)
}
return nil
}
// Pattern: Include resource identifiers
func processUser(userID int) error {
user, err := db.GetUser(userID)
if err != nil {
return fmt.Errorf("get user: %w, userID=%d", err, userID)
}
if err := validateUser(user); err != nil {
return fmt.Errorf("validate user: %w, userID=%d", err, userID)
}
return nil
}
// HTTP Handler
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
user, err := h.service.GetUser(userID)
if err != nil {
log.Printf("get user failed: %v", err) // Log full error
http.Error(w, "user not found", 404) // Return safe error
return
}
// ... success handling
}
// Service Layer
func (s *UserService) GetUser(userID int) (*User, error) {
user, err := s.repo.GetUser(userID)
if err != nil {
return nil, fmt.Errorf("get user: %w, userID=%d", err, userID)
}
return user, nil
}
// Repository Layer
func (r *UserRepo) GetUser(userID int) (*User, error) {
user, err := r.db.QueryUser(userID)
if err != nil {
return nil, fmt.Errorf("query user: %w, userID=%d", err, userID)
}
return user, nil
}