Go

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

All 37 Error Handling & panics questions