When should you wrap an error and when should you not? How do you handle errors at package boundaries?
Question 304HardGo 1.22 to 1.25
Wrap (%w) when callers need to inspect the cause and the cause is part of your stable contract. Don't wrap (use %v, or return your own error) when the cause is an implementation detail. If your UserRepo wraps sql.ErrNoRows, callers begin checking for it, and you can no longer swap SQL for Redis without breaking them.
var ErrUserNotFound = errors.New("user not found")
func (r *Repo) Get(ctx context.Context, id int64) (*User, error) {
var u User
err := r.db.QueryRowContext(ctx, q, id).Scan(&u.ID, &u.Name)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrUserNotFound // translate: hide the storage detail
case err != nil:
return nil, fmt.Errorf("repo.Get %d: %v", id, err) // context, not API
}
return &u, nil
}
Guidelines: add context that the caller doesn't already have (the operation, key IDs) and don't repeat what the inner error already says. Keep messages short and lower case, like "open config: read x.yaml: permission denied". Avoid noise like "failed to...", which piles up at every layer. At service edges (HTTP, gRPC), map domain errors to status codes and never leak internal messages to clients.
More on Error Handling & panics
- Q302The nil error interface gotcha: what does this print?
- Q303When is
err == ErrXwrong, and why can comparing errors with==panic? - Q305When is it appropriate to
panicinstead of returning an error? - Q306What does this print? (Rules of
recover) - Q307A goroutine panics. Can a
recoverinmaincatch it? How do you protect background goroutines? - Q308What does this print? (Defer order during panic)