| name | golang-error-handling |
| description | Error handling patterns for Go including error wrapping, custom errors, sentinel errors, and error checking best practices. Use when implementing error handling, defining error types, or working with error flows. Use when this capability is needed. |
| metadata | {"author":"codeready-toolchain"} |
Go Error Handling
Modern error handling patterns for Go following 2025-2026 best practices.
Basic Error Handling
Always check errors:
result, err := doSomething()
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
Never ignore errors (unless explicitly documented):
doSomething()
_ = file.Close()
defer func() {
if err := file.Close(); err != nil {
log.Printf("failed to close file: %v", err)
}
}()
Error Wrapping
Use %w to wrap errors:
func GetUser(id string) (*User, error) {
user, err := db.Query(id)
if err != nil {
return nil, fmt.Errorf("failed to get user %s: %w", id, err)
}
return user, nil
}
Unwrap errors with errors.Is() and errors.As():
err := GetUser("123")
if errors.Is(err, sql.ErrNoRows) {
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("field: %s, message: %s", validationErr.Field, validationErr.Message)
}
Sentinel Errors
Define package-level error values:
package services
import "errors"
var (
ErrNotFound = errors.New("resource not found")
ErrAlreadyExists = errors.New("resource already exists")
ErrInvalidInput = errors.New("invalid input")
ErrConflict = errors.New("resource conflict")
ErrUnauthorized = errors.New("unauthorized")
)
Check with errors.Is():
session, err := service.GetSession(ctx, id)
if errors.Is(err, services.ErrNotFound) {
return http.StatusNotFound, "session not found"
}
if err != nil {
return http.StatusInternalServerError, "internal error"
}
Custom Error Types
For errors needing additional context:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s %s", e.Field, e.Message)
}
func NewValidationError(field, message string) error {
return &ValidationError{Field: field, Message: message}
}
if req.SessionID == "" {
return nil, NewValidationError("session_id", "required")
}
Error type with wrapped cause:
type DatabaseError struct {
Operation string
Cause error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error during %s: %v", e.Operation, e.Cause)
}
func (e *DatabaseError) Unwrap() error {
return e.Cause
}
if err := saveData(); err != nil {
return &DatabaseError{
Operation: "save",
Cause: err,
}
}
Error Checking Patterns
Check and return early:
func ProcessData(data string) error {
if data == "" {
return ErrInvalidInput
}
result, err := transform(data)
if err != nil {
return fmt.Errorf("transform failed: %w", err)
}
if err := save(result); err != nil {
return fmt.Errorf("save failed: %w", err)
}
return nil
}
Multiple return values:
func GetUser(id string) (*User, error) {
}
func GetUser(id string) (*User, bool, error) {
}
Error Context
Add context when wrapping:
func CreateSession(ctx context.Context, req Request) (*Session, error) {
session, err := buildSession(req)
if err != nil {
return nil, fmt.Errorf("failed to build session: %w", err)
}
if err := s.repo.Save(ctx, session); err != nil {
return nil, fmt.Errorf("failed to save session %s: %w", session.ID, err)
}
return session, nil
}
Progressive error context:
func parseConfig(data []byte) (*Config, error) {
}
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
cfg, err := parseConfig(data)
if err != nil {
return nil, fmt.Errorf("parse config from %s: %w", path, err)
}
return cfg, nil
}
func initializeApp() error {
cfg, err := loadConfig("/etc/app/config.json")
if err != nil {
return fmt.Errorf("failed to initialize app: %w", err)
}
}
Error Handling in Transactions
Rollback on error, handle commit failure:
func (s *Service) Transaction(ctx context.Context) error {
tx, err := s.client.Tx(ctx)
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
defer func() { _ = tx.Rollback() }()
if err := tx.Entity.Create().Save(ctx); err != nil {
return fmt.Errorf("failed to create entity: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
Multiple Error Handling
Using errors.Join():
func ValidateRequest(req Request) error {
var errs []error
if req.Name == "" {
errs = append(errs, NewValidationError("name", "required"))
}
if req.Email == "" {
errs = append(errs, NewValidationError("email", "required"))
}
if req.Age < 0 {
errs = append(errs, NewValidationError("age", "must be positive"))
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
err := ValidateRequest(req)
if err != nil {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
}
}
Error Logging
Log at the right level:
func ProcessRequest(ctx context.Context, req Request) error {
session, err := s.GetSession(ctx, req.SessionID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return fmt.Errorf("session not found: %w", err)
}
log.Printf("ERROR: failed to get session: %v", err)
return fmt.Errorf("internal error: %w", err)
}
return nil
}
Don't log and return:
func GetUser(id string) (*User, error) {
user, err := db.Query(id)
if err != nil {
log.Printf("ERROR: failed to query user: %v", err)
return nil, err
}
return user, nil
}
func GetUser(id string) (*User, error) {
user, err := db.Query(id)
if err != nil {
return nil, fmt.Errorf("query user %s: %w", id, err)
}
return user, nil
}
func Handler(w http.ResponseWriter, r *http.Request) {
user, err := service.GetUser(id)
if err != nil {
log.Printf("ERROR: %v", err)
http.Error(w, "internal error", 500)
return
}
}
Error Conversion for APIs
Convert internal errors to HTTP responses:
func ErrorToHTTPStatus(err error) (int, string) {
if err == nil {
return http.StatusOK, ""
}
if errors.Is(err, services.ErrNotFound) {
return http.StatusNotFound, "resource not found"
}
if errors.Is(err, services.ErrAlreadyExists) {
return http.StatusConflict, "resource already exists"
}
if errors.Is(err, services.ErrInvalidInput) {
return http.StatusBadRequest, "invalid input"
}
var validationErr *services.ValidationError
if errors.As(err, &validationErr) {
return http.StatusBadRequest, fmt.Sprintf("invalid %s: %s",
validationErr.Field, validationErr.Message)
}
return http.StatusInternalServerError, "internal server error"
}
func (h *Handler) CreateSession(w http.ResponseWriter, r *http.Request) {
session, err := h.service.CreateSession(r.Context(), req)
if err != nil {
status, message := ErrorToHTTPStatus(err)
http.Error(w, message, status)
return
}
json.NewEncoder(w).Encode(session)
}
Panic and Recover
Use recover only at boundaries:
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC: %v\n%s", r, debug.Stack())
http.Error(w, "internal server error", 500)
}
}()
}
Don't use panic for normal errors:
func MustGetConfig() *Config {
cfg, err := loadConfig()
if err != nil {
panic(err)
}
return cfg
}
func GetConfig() (*Config, error) {
cfg, err := loadConfig()
if err != nil {
return nil, fmt.Errorf("load config: %w", err)
}
return cfg, nil
}
TARSy-Specific Patterns
Service layer error definitions:
package services
var (
ErrNotFound = errors.New("resource not found")
ErrAlreadyExists = errors.New("resource already exists")
ErrInvalidInput = errors.New("invalid input")
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s %s", e.Field, e.Message)
}
func NewValidationError(field, message string) error {
return &ValidationError{Field: field, Message: message}
}
Service method error handling:
func (s *SessionService) CreateSession(ctx context.Context, req CreateSessionRequest) (*ent.AlertSession, error) {
if req.SessionID == "" {
return nil, NewValidationError("session_id", "required")
}
session, err := s.client.AlertSession.Create().
SetID(req.SessionID).
Save(ctx)
if err != nil {
if ent.IsConstraintError(err) {
return nil, fmt.Errorf("session %s: %w", req.SessionID, ErrAlreadyExists)
}
return nil, fmt.Errorf("failed to create session: %w", err)
}
return session, nil
}
Quick Reference
Error creation:
errors.New("message")
fmt.Errorf("format: %w", err)
&CustomError{Field: "value"}
Error checking:
errors.Is(err, ErrNotFound)
errors.As(err, &validationErr)
err != nil
Best practices:
- Always check errors (except documented safe ignores)
- Wrap errors with context using
%w
- Define sentinel errors for common cases
- Use custom types for errors needing data
- Check with
errors.Is() and errors.As()
- Add context progressively up the call stack
- Log errors once at appropriate level
- Convert internal errors for API responses
Source: codeready-toolchain/claw-operator — distributed by TomeVault.