Error Handling
Go treats errors as values, not exceptions. Functions return an error alongside their result, and callers check it immediately. There is no try/catch.
The error Interface
type error interface {
Error() string
}
Any type with an Error() string method is an error. The most common way to create one:
import "errors"
err := errors.New("something went wrong")
err := fmt.Errorf("failed to load user %d: %w", id, err)
The if err != nil Pattern
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("reading config: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("parsing config: %w", err)
}
Every function call that can fail gets its own error check. This is verbose but explicit - you always know what happens when something fails.
Tip: Wrap errors with context about what YOU were doing, not what the callee did.
"reading config: %w"not"ReadFile failed: %w".
Error Wrapping with %w
fmt.Errorf with the %w verb wraps an error, preserving the original for inspection:
func loadUser(id int) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
var u User
if err := row.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return nil, fmt.Errorf("load user %d: %w", id, err)
}
return &u, nil
}
The wrapped error chain is inspectable with errors.Is and errors.As.
Sentinel Errors with errors.Is
Sentinel errors are package-level variables for expected error conditions:
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func GetUser(id string) (*User, error) {
u, ok := users[id]
if !ok {
return nil, fmt.Errorf("get user %s: %w", id, ErrNotFound)
}
return u, nil
}
// Caller checks with errors.Is (works through wrapping):
user, err := GetUser("abc")
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
Gotcha: Always use
errors.Is(err, target), nevererr == target. The==comparison does not unwrap.
Custom Error Types with errors.As
For errors that carry structured data:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s %s", e.Field, e.Message)
}
func ValidateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "must be between 0 and 150",
}
}
return nil
}
// Extract the typed error:
if err := ValidateAge(-1); err != nil {
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("field %s: %s\n", ve.Field, ve.Message)
}
}
Multiple Wrapping (Go 1.20+)
Wrap multiple errors with %w:
err := fmt.Errorf("operation failed: %w and %w", err1, err2)
errors.Is(err, err1) // true
errors.Is(err, err2) // true
Or join them:
err := errors.Join(err1, err2, err3)
This is useful for validation that collects multiple errors.
Error Handling Rules
- Handle each error exactly once. Either return it, log it, or handle it. Never log and return the same error.
// Bad: error gets logged twice (here and by caller)
if err != nil {
log.Println("failed:", err)
return err
}
// Good: add context and return
if err != nil {
return fmt.Errorf("fetching user: %w", err)
}
- Sanitize at API boundaries. Don’t leak internal errors to external callers.
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
user, err := s.store.GetUser(r.Context(), chi.URLParam(r, "id"))
if err != nil {
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
// Log the internal error, return a generic message
slog.Error("get user failed", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
- Use
deferfor cleanup, not error handling.
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // always runs, even on error return
When to Panic
Almost never. Panics are for unrecoverable programmer errors (out of bounds, nil dereference in impossible situations). Library code should never panic - return errors instead.
The only acceptable uses:
Must*wrapper functions that panic on error (for package-level initialization)- Truly impossible states that indicate a bug
var tmpl = template.Must(template.ParseFiles("index.html"))
Next: Interfaces | Testing