Go

How would you design an error model for a large service, for example with Op, Kind and a wrapped cause?

Question 324HardGo 1.22 to 1.25

A common senior-level design, inspired by Upspin and many internal Google and Uber codebases, is a single structured error type. It carries a machine-readable Kind (used for control flow and protocol mapping), an Op (where the failure happened), and the wrapped cause (detail for debugging):

type Kind uint8

const (
	KindOther Kind = iota
	KindNotFound
	KindInvalid
	KindPermission
	KindConflict
)

type Error struct {
	Op   string // "userSvc.Update"
	Kind Kind
	Err  error
}

func (e *Error) Error() string { return e.Op + ": " + e.Err.Error() }
func (e *Error) Unwrap() error { return e.Err }

func E(op string, k Kind, err error) error { return &Error{Op: op, Kind: k, Err: err} }

// KindOf returns the outermost non-Other kind in the chain.
func KindOf(err error) Kind {
	var e *Error
	for errors.As(err, &e) {
		if e.Kind != KindOther {
			return e.Kind
		}
		err = e.Err
	}
	return KindOther
}

func httpStatus(err error) int {
	switch KindOf(err) {
	case KindNotFound:
		return http.StatusNotFound
	case KindInvalid:
		return http.StatusBadRequest
	case KindPermission:
		return http.StatusForbidden
	case KindConflict:
		return http.StatusConflict
	}
	return http.StatusInternalServerError
}

What to point out: kinds are a closed set that maps onto HTTP and gRPC codes in one place. User-safe messages stay separate from internal detail. The model works with errors.Is/As. Logging happens once, at the edge. Avoid putting request-specific values into sentinel messages.

More on Error Handling & panics

All 37 Error Handling & panics questions