| 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. |
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