Go handles errors explicitly through return values rather than exceptions. Errors are values that implement the error interface, making error handling straightforward and visible in your code.
💡 Key Points
- Errors are values, not exceptions
- Functions return errors as the last return value
- Always check errors immediately after function calls
- The
error interface has one method: Error() string - Use
errors.New() to create simple errors - Use
fmt.Errorf() for formatted error messages - Wrap errors with
%w to preserve error chain - Use
errors.Is() and errors.As() for error checking
Basic Error Handling
Functions return errors that must be explicitly checked:
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
// Proper error checking
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
// Error case
result, err = divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
Output:Result: 5
Error: division by zero
Custom Errors
Create custom error types for more context:
package main
import "fmt"
// Custom error type
type ValidationError struct {
Field string
Message string
}
// Implement error interface
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on '%s': %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "cannot be negative",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Message: "unrealistic value",
}
}
return nil
}
func main() {
// Valid age
if err := validateAge(30); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Age is valid")
}
// Invalid age
if err := validateAge(-5); err != nil {
fmt.Println("Error:", err)
}
// Check specific error type
err := validateAge(200)
if validErr, ok := err.(*ValidationError); ok {
fmt.Printf("Field '%s' failed: %s\n", validErr.Field, validErr.Message)
}
}
Output:Age is valid
Error: validation error on 'age': cannot be negative
Field 'age' failed: unrealistic value
Error Wrapping and Unwrapping
Wrap errors to add context while preserving the error chain:
package main
import (
"errors"
"fmt"
"os"
)
var ErrNotFound = errors.New("not found")
func readConfig(filename string) error {
_, err := os.Open(filename)
if err != nil {
// Wrap error with context using %w
return fmt.Errorf("failed to read config: %w", err)
}
return nil
}
func initialize() error {
err := readConfig("config.json")
if err != nil {
return fmt.Errorf("initialization failed: %w", err)
}
return nil
}
func main() {
err := initialize()
if err != nil {
fmt.Println("Error:", err)
// Check if error chain contains specific error
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Config file does not exist")
}
// Unwrap to get underlying error
fmt.Println("Unwrapped:", errors.Unwrap(err))
}
}
Output:Error: initialization failed: failed to read config: open config.json: no such file or directory
Config file does not exist
Unwrapped: failed to read config: open config.json: no such file or directory
Error Checking Functions
| Function | Signature | Purpose |
errors.New | func New(text string) error | Create simple error |
fmt.Errorf | func Errorf(format string, a ...any) error | Create formatted error |
errors.Is | func Is(err, target error) bool | Check if error matches target |
errors.As | func As(err error, target any) bool | Check if error is of type |
errors.Unwrap | func Unwrap(err error) error | Get wrapped error |
errors.Join | func Join(errs ...error) error | Combine multiple errors (Go 1.20+) |
Error Handling Patterns
| Pattern | Code | Use Case |
| Immediate check | if err != nil { return err } | Most common pattern |
| Inline check | if err := fn(); err != nil { } | Limit error scope |
| Wrap and return | return fmt.Errorf("context: %w", err) | Add context to error |
| Sentinel error | var ErrNotFound = errors.New("not found") | Define known errors |
| Error variable | var err error | Reuse across calls |
| Named return | func fn() (err error) { defer ... } | Clean up on error |
Panic and Recover
| Function | Description | When to Use |
panic(v) | Stop normal execution | Unrecoverable errors, programming bugs |
recover() | Regain control after panic | In deferred functions to catch panics |
defer | Execute before function returns | Cleanup, recover from panics |
Error Best Practices
| Practice | Description | Example |
| Check all errors | Never ignore error values | Always use if err != nil |
| Add context | Wrap errors with meaningful messages | fmt.Errorf("failed to X: %w", err) |
| Use %w for wrapping | Preserve error chain for Is/As | fmt.Errorf("context: %w", err) |
| Return early | Handle errors immediately | if err != nil { return err } |
| Descriptive messages | Error messages should be clear | "failed to connect to database" |
| Lowercase messages | Error strings start lowercase | errors.New("connection failed") |
| No punctuation | Don't end with period | "not found" not "not found." |
| Define sentinel errors | Use variables for known errors | var ErrNotFound = ... |
| Custom types for context | Add fields to error structs | Include codes, fields, etc. |
| Avoid panic | Use panic only for bugs | Return errors instead |
Common Error Patterns
| Pattern | Example | Use Case |
| Error as last return | func Open() (*File, error) | Standard Go convention |
| Boolean ok pattern | v, ok := m[key] | Map lookups, type assertions |
| Defer cleanup | defer file.Close() | Ensure cleanup happens |
| Error group | errgroup.Group | Collect errors from goroutines |
| Multi-error | errors.Join(err1, err2) | Multiple errors at once |