type error interface {
Error() string
}
Any type implementing Error() string satisfies the error interface.
import "errors"
var err1 = errors.New("something went wrong")
// fmt.Errorf for formatted messages (no wrapping)
err2 := fmt.Errorf("user %d not found", id)
// Return nil to signal success
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Check error
result, err := divide(10, 0)
if err != nil {
log.Fatal(err)
}
The %w verb creates a wrapped error that preserves the original for inspection with errors.Is and errors.As.
func getUser(id int64) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
if err := row.Scan(&user); err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return &user, nil
}
func loadProfile(id int64) (*Profile, error) {
user, err := getUser(id)
if err != nil {
return nil, fmt.Errorf("load profile: %w", err)
}
// ...
return profile, nil
}
The resulting error chain looks like:
load profile: get user 42: sql: no rows in result set
// errors.Unwrap returns the next error in the chain
wrapped := fmt.Errorf("outer: %w", inner)
inner == errors.Unwrap(wrapped) // true
// Walk the full chain manually
for err != nil {
fmt.Println(err)
err = errors.Unwrap(err)
}
Use errors.Is to check whether a specific sentinel error appears anywhere in the chain.
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("query: %w", ErrNotFound)
errors.Is(err, ErrNotFound) // true — searches the whole chain
err == ErrNotFound // false — direct comparison misses the wrapping
Use errors.As to extract a typed error from anywhere in the chain.
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
err := fmt.Errorf("create user: %w", &ValidationError{Field: "email", Message: "invalid"})
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println(valErr.Field) // "email"
fmt.Println(valErr.Message) // "invalid"
}
Implement Is when equality should be value-based rather than pointer-based.
type StatusError struct {
Code int
}
func (e *StatusError) Error() string {
return fmt.Sprintf("status %d", e.Code)
}
func (e *StatusError) Is(target error) bool {
t, ok := target.(*StatusError)
if !ok {
return false
}
return e.Code == t.Code
}
ErrNotFound := &StatusError{Code: 404}
err := fmt.Errorf("request: %w", &StatusError{Code: 404})
errors.Is(err, ErrNotFound) // true — matched by value
Sentinel errors are package-level variables used as well-known error values.
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
func FindUser(id int64) (*User, error) {
if id == 0 {
return nil, ErrNotFound
}
// ...
}
// Caller checks identity
user, err := FindUser(id)
if errors.Is(err, ErrNotFound) {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
io.EOF // end of stream; not an error condition
io.ErrUnexpectedEOF // stream ended mid-record; is an error
sql.ErrNoRows // query returned zero rows
os.ErrNotExist // file does not exist (use errors.Is, not ==)
context.Canceled // context was cancelled
context.DeadlineExceeded // context deadline passed
Note: os.ErrNotExist wraps multiple underlying errors (syscall.ENOENT, etc.). Always use errors.Is(err, os.ErrNotExist) rather than direct comparison.
type NotFoundError struct {
Resource string
ID int64
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
}
func GetOrder(id int64) (*Order, error) {
order := findOrder(id)
if order == nil {
return nil, &NotFoundError{Resource: "order", ID: id}
}
return order, nil
}
// Extract and use the extra fields
var notFound *NotFoundError
if errors.As(err, ¬Found) {
log.Printf("missing resource: %s %d", notFound.Resource, notFound.ID)
}
type HTTPError struct {
Code int
Message string
Cause error
}
func (e *HTTPError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("HTTP %d: %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}
func (e *HTTPError) Unwrap() error {
return e.Cause
}
// Implement Unwrap to keep the chain intact
// Repository layer: wrap with operation context
func (r *UserRepo) Find(id int64) (*User, error) {
var u User
err := r.db.Get(&u, "SELECT * FROM users WHERE id = $1", id)
if err != nil {
return nil, fmt.Errorf("find user %d: %w", id, err)
}
return &u, nil
}
// Service layer: wrap with business operation context
func (s *UserService) GetProfile(id int64) (*Profile, error) {
user, err := s.repo.Find(id)
if err != nil {
return nil, fmt.Errorf("get profile: %w", err)
}
// ...
}
// Handler layer: inspect and translate for the caller
func (h *Handler) handleGetProfile(w http.ResponseWriter, r *http.Request) {
id := parseID(r)
profile, err := h.svc.GetProfile(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
log.Printf("get profile: %v", err) // log full chain here
return
}
writeJSON(w, profile)
}
: to separate context from cause// GOOD
fmt.Errorf("parse config: %w", err)
fmt.Errorf("connect to database %s: %w", dsn, err)
// BAD: redundant — the wrapped error already says "failed"
fmt.Errorf("failed to connect: %w", err)
// BAD: capitalized
fmt.Errorf("Parse config: %w", err)
// BAD: logged at every layer, duplicates output
func (r *Repo) Find(id int64) (*User, error) {
err := query()
if err != nil {
log.Printf("repo error: %v", err) // logged here
return nil, fmt.Errorf("find: %w", err)
}
}
func (s *Svc) Get(id int64) (*User, error) {
u, err := r.Find(id)
if err != nil {
log.Printf("svc error: %v", err) // logged again
return nil, fmt.Errorf("get: %w", err)
}
}
// GOOD: wrap through; log once at the edge (handler/main)
init or package var blocks)func mustParseURL(raw string) *url.URL {
u, err := url.Parse(raw)
if err != nil {
panic(fmt.Sprintf("invalid hardcoded URL %q: %v", raw, err))
}
return u
}
// Use Must* pattern for hardcoded values only; never for user input
var baseURL = mustParseURL("https://api.example.com")
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
// Log with stack trace
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
log.Printf("panic recovered: %v\n%s", rec, buf[:n])
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
A library must never let a panic escape to the caller. Recover internally and return an error.
func (p *Parser) Parse(input []byte) (result Result, err error) {
defer func() {
if rec := recover(); rec != nil {
err = fmt.Errorf("parse panicked: %v", rec)
}
}()
result = p.doParse(input)
return
}
func runAsync(ctx context.Context, fn func() error) <-chan error {
errCh := make(chan error, 1) // buffer of 1 prevents leak
go func() {
errCh <- fn()
}()
return errCh
}
errCh := runAsync(ctx, doWork)
select {
case err := <-errCh:
if err != nil {
return fmt.Errorf("async work: %w", err)
}
case <-ctx.Done():
return ctx.Err()
}
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return stepA(ctx)
})
g.Go(func() error {
return stepB(ctx)
})
// Wait returns the first non-nil error; other goroutines see ctx cancelled
if err := g.Wait(); err != nil {
return fmt.Errorf("pipeline: %w", err)
}
When all errors matter (not just the first):
type MultiError struct {
Errors []error
}
func (m *MultiError) Error() string {
msgs := make([]string, len(m.Errors))
for i, e := range m.Errors {
msgs[i] = e.Error()
}
return strings.Join(msgs, "; ")
}
func runAll(fns []func() error) error {
var mu sync.Mutex
var errs []error
var wg sync.WaitGroup
for _, fn := range fns {
fn := fn
wg.Add(1)
go func() {
defer wg.Done()
if err := fn(); err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
}()
}
wg.Wait()
if len(errs) > 0 {
return &MultiError{Errors: errs}
}
return nil
}
err1 := errors.New("validation failed on field email")
err2 := errors.New("validation failed on field phone")
combined := errors.Join(err1, err2)
fmt.Println(combined)
// validation failed on field email
// validation failed on field phone
errors.Is(combined, err1) // true
errors.Is(combined, err2) // true
func validateUser(u User) error {
var errs []error
if u.Name == "" {
errs = append(errs, errors.New("name is required"))
}
if !isValidEmail(u.Email) {
errs = append(errs, fmt.Errorf("email %q is invalid", u.Email))
}
if u.Age < 0 {
errs = append(errs, errors.New("age must not be negative"))
}
return errors.Join(errs...) // nil if errs is empty
}
func TestFindUser_NotFound(t *testing.T) {
repo := NewRepo(testDB)
_, err := repo.Find(999)
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got %v", err)
}
}
func TestValidate_InvalidEmail(t *testing.T) {
err := validateUser(User{Name: "Alice", Email: "bad"})
var valErr *ValidationError
if !errors.As(err, &valErr) {
t.Fatalf("expected *ValidationError, got %T: %v", err, err)
}
if valErr.Field != "email" {
t.Errorf("expected field 'email', got %q", valErr.Field)
}
}
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
wantErr bool
errIs error
}{
{"normal", 10, 2, false, nil},
{"divide by zero", 10, 0, true, ErrDivisionByZero},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Fatalf("wantErr=%v, got err=%v", tt.wantErr, err)
}
if tt.errIs != nil && !errors.Is(err, tt.errIs) {
t.Errorf("errors.Is(%v, %v) = false", err, tt.errIs)
}
})
}
}
// BAD: fragile; breaks on message change
if err.Error() == "not found" { ... }
if strings.Contains(err.Error(), "timeout") { ... }
// GOOD: use sentinel or typed errors
if errors.Is(err, ErrNotFound) { ... }
var netErr *net.OpError
if errors.As(err, &netErr) && netErr.Timeout() { ... }
// BAD: silent discard
result, _ := doSomething()
json.Unmarshal(data, &v) // ignoring error
// GOOD: handle or at minimum log
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething: %w", err)
}
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
}
// BAD: causes duplicate log lines
func getUser(id int64) (*User, error) {
user, err := db.Find(id)
if err != nil {
log.Printf("db.Find error: %v", err) // logged here
return nil, fmt.Errorf("find user: %w", err)
}
return user, nil
}
// caller also logs → same error appears twice
// GOOD: wrap and propagate; log once at the boundary
func getUser(id int64) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf("find user %d: %w", id, err)
}
return user, nil
}
// BAD: "failed to" is noise; wrapped error already explains what happened
return fmt.Errorf("failed to get user: failed to query database: %w", err)
// GOOD: each layer adds one meaningful label
return fmt.Errorf("get user %d: %w", id, err)
// BAD: panicking on user-controlled input
func ParseAge(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic("invalid age") // crashes the program
}
return n
}
// GOOD: return the error
func ParseAge(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse age %q: %w", s, err)
}
return n, nil
}
// BAD: caller may use the value even when err != nil
func compute() (Result, error) {
if bad {
return Result{partial: true}, errors.New("incomplete")
}
}
// GOOD: return zero value on error so callers don't accidentally use it
func compute() (Result, error) {
if bad {
return Result{}, errors.New("incomplete")
}
}