Go Error Handling
The error Interface
Errors are values, not exceptions
// The error interface
type error interface {
Error() string
}
// Check errors immediately after the call
f, err := os.Open("/etc/hosts")
if err != nil {
log.Fatal(err)
}
defer f.Close()
Go has no try/catch. Errors are returned as the last value. The caller decides what to do.
Creating Errors
errors.New and fmt.Errorf
// Sentinel error — package-level variable
var ErrNotFound = errors.New("not found")
// Formatted error with context
func findHost(name string) (*Host, error) {
return nil, fmt.Errorf("host %q not found in inventory", name)
}
Wrapping Errors
%w wraps an error, preserving the chain
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("readConfig: %w", err)
}
return data, nil
}
// Caller unwraps to find root cause
if errors.Is(err, os.ErrNotExist) {
fmt.Println("config file missing")
}
%w (not %v) wraps the error so errors.Is and errors.As can traverse the chain.
errors.Is and errors.As
Check identity and extract typed errors
// errors.Is — does any error in the chain match?
if errors.Is(err, os.ErrPermission) {
fmt.Println("permission denied")
}
// errors.As — extract a specific error type
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("op %s on %s: %v\n", pathErr.Op, pathErr.Path, pathErr.Err)
}
errors.Is checks for identity (sentinel errors). errors.As checks for type and extracts the value. Both traverse the full wrap chain.
Custom Error Types
Implement error for structured errors
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s — %s", e.Field, e.Message)
}
func validatePort(port int) error {
if port < 1 || port > 65535 {
return &ValidationError{
Field: "port",
Message: fmt.Sprintf("%d out of range [1-65535]", port),
}
}
return nil
}
Panic and Recover
panic for unrecoverable bugs, recover at API boundaries
// panic — programmer errors only
func MustParse(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("MustParse(%q): %v", s, err))
}
return n
}
// recover — catch panics at boundaries
func safeHandler(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered: %v", r)
http.Error(w, "internal error", 500)
}
}()
// handler code
}
panic is for bugs, not expected failures. Return error for everything else.
Idiomatic Pattern
Happy path down the left margin
func processFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read: %w", err)
}
result, err := parse(data)
if err != nil {
return fmt.Errorf("parse: %w", err)
}
if err := save(result); err != nil {
return fmt.Errorf("save: %w", err)
}
return nil
}
Each error check adds context and returns immediately. The happy path flows straight down the left margin.