easyBackend EngineerTechnology
How does Go's error handling differ from exceptions in other languages, and what are the best practices?
Posted 18/04/2026
by Mehedy Hasan Ador
Question Details
At a company with Go microservices:
> "Our Go code has
> "Our Go code has
if err != nil on almost every line. It feels verbose. Why doesn't Go have exceptions like Python or Java? What's the idiomatic approach?"Suggested Solution
Go's Error Philosophy
Go treats errors as values, not exceptions. No try/catch, no stack unwinding.// Go: errors are returned values
result, err := doSomething()
if err != nil {
return fmt.Errorf("failed to do something: %w", err)
}
// Python: exceptions
try:
result = do_something()
except Exception as e:
raise ValueError(f"Failed: {e}")
Why No Exceptions?
Best Practices
1. Wrap errors with context
func GetUser(id string) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
var user User
if err := row.Scan(&user); err != nil {
return nil, fmt.Errorf("GetUser(%s): %w", id, err)
}
return &user, nil
}
// Error: GetUser(123): sql: no rows in result set
2. Custom error types
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %s not found", e.Resource, e.ID)
}
// Check with errors.Is / errors.As
var notFound *NotFoundError
if errors.As(err, ¬Found) {
return http.StatusNotFound
}
3. Defer for cleanup
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // Always runs, even on error
return io.ReadAll(f)
}
4. Sentry/bubble up with %w
// Use %w to wrap (preserves error chain for errors.Is/As)
return fmt.Errorf("process user: %w", err)
// Use %v for just message (breaks chain)
return fmt.Errorf("process user: %v", err)