Created
January 17, 2026 00:25
-
-
Save brandonhimpfen/213d21e83bed6b92d49670e45d2d6ffb to your computer and use it in GitHub Desktop.
Go custom error wrapping pattern using %w, errors.Is/As, and optional typed sentinel errors (clean, idiomatic, and debuggable).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "errors" | |
| "fmt" | |
| ) | |
| // Sentinel errors (useful for high-level classification) | |
| var ( | |
| ErrNotFound = errors.New("not found") | |
| ErrInvalidInput = errors.New("invalid input") | |
| ) | |
| // Custom typed error that wraps an underlying cause. | |
| // This lets callers use errors.As() to extract structured info. | |
| type AppError struct { | |
| Op string // operation name (e.g., "db.GetUser") | |
| Code error // optional sentinel category (e.g., ErrNotFound) | |
| Err error // underlying error (wrapped) | |
| } | |
| func (e *AppError) Error() string { | |
| // Keep it readable for logs | |
| if e.Code != nil { | |
| return fmt.Sprintf("%s: %v: %v", e.Op, e.Code, e.Err) | |
| } | |
| return fmt.Sprintf("%s: %v", e.Op, e.Err) | |
| } | |
| // Unwrap enables errors.Is / errors.As to traverse the chain. | |
| func (e *AppError) Unwrap() error { return e.Err } | |
| // Is lets errors.Is(err, ErrNotFound) match when AppError.Code == ErrNotFound. | |
| // This is optional but convenient when you want a "category" error. | |
| func (e *AppError) Is(target error) bool { | |
| return e.Code != nil && target == e.Code | |
| } | |
| // Helper constructor to standardize creation and wrapping. | |
| func Wrap(op string, code error, err error) error { | |
| if err == nil { | |
| return nil | |
| } | |
| return &AppError{Op: op, Code: code, Err: err} | |
| } | |
| // ---------------------------------------------------------------------- | |
| // Example usage | |
| // ---------------------------------------------------------------------- | |
| func getUser(id string) (string, error) { | |
| const op = "service.getUser" | |
| if id == "" { | |
| return "", Wrap(op, ErrInvalidInput, fmt.Errorf("id is empty")) | |
| } | |
| // Simulate a lower-level error (e.g., from db driver) | |
| dbErr := fmt.Errorf("query users where id=%q: %w", id, ErrNotFound) | |
| // Wrap it with operation + category. | |
| return "", Wrap(op, ErrNotFound, dbErr) | |
| } | |
| func main() { | |
| _, err := getUser("123") | |
| if err != nil { | |
| fmt.Println("error:", err) | |
| // Classification via sentinel errors | |
| if errors.Is(err, ErrNotFound) { | |
| fmt.Println("-> handle 404 / not found") | |
| } | |
| // Extract structured info | |
| var appErr *AppError | |
| if errors.As(err, &appErr) { | |
| fmt.Println("-> operation:", appErr.Op) | |
| if appErr.Code != nil { | |
| fmt.Println("-> category:", appErr.Code) | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment