Error handling is a critical aspect of writing reliable Go programs, yet it can be intimidating for beginners. In this guide, we’ll demystify Go’s error handling patterns and show you how to write more robust code, even if you’re just starting your Go journey.
If you’re new to Go programming, you might want to check out our Getting Started with Go guide first to get familiar with the basics.
Table of Contents
- Understanding Go’s Error Philosophy
- Creating and Checking Errors
- Custom Error Types
- Error Wrapping
- Best Practices for Error Handling
- Error Handling Patterns
- Testing Error Handling
Understanding Go’s Error Philosophy
Unlike many other programming languages that use exceptions for error handling, Go takes a simpler approach by treating errors as values. This design choice makes error handling explicit and encourages developers to think about possible failure cases.
In Go, functions that can fail typically return an error as their last return value. Here’s a basic example:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Code language: PHP (php)
Creating and Checking Errors
Go provides several ways to create errors. Let’s explore the most common approaches:
Using errors.New()
The simplest way to create an error is using errors.New()
:
package main
import (
"errors"
"fmt"
)
func validateAge(age int) error {
if age < 0 {
return errors.New("age cannot be negative")
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
fmt.Println("Error:", err)
}
}
Code language: JavaScript (javascript)
Using fmt.Errorf()
When you need to format error messages with variables, fmt.Errorf()
is your friend:
func validateUsername(username string) error {
if len(username) < 3 {
return fmt.Errorf("username must be at least 3 characters long, got %d", len(username))
}
return nil
}
Code language: JavaScript (javascript)
Custom Error Types
For more complex applications, you might want to create custom error types. This allows you to provide more context and handle specific error cases differently:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validateUser(username string, age int) error {
if len(username) < 3 {
return &ValidationError{
Field: "username",
Message: "must be at least 3 characters long",
}
}
if age < 0 {
return &ValidationError{
Field: "age",
Message: "cannot be negative",
}
}
return nil
}
Code language: JavaScript (javascript)
Error Wrapping
Go 1.13 introduced error wrapping, which helps maintain error context through the call stack:
package main
import (
"fmt"
"os"
)
func readConfig(path string) error {
_, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
return nil
}
func main() {
err := readConfig("config.json")
if err != nil {
fmt.Println(err)
// Check if it's a specific type of error
if os.IsNotExist(err) {
fmt.Println("The config file doesn't exist!")
}
}
}
Code language: JavaScript (javascript)
Best Practices for Error Handling
- Always Check Error Returns
Don’t ignore returned errors. Either handle them or explicitly return them:
file, err := os.Open("file.txt")
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
Code language: JavaScript (javascript)
- Use Descriptive Error Messages
Make your error messages clear and actionable:
// Bad
return errors.New("invalid input")
// Good
return fmt.Errorf("username must be between 3 and 20 characters, got %d", len(username))
Code language: PHP (php)
- Wrap Errors with Context
Add context when returning errors from deeper call stacks:
func processUserData(userData []byte) error {
user, err := parseUser(userData)
if err != nil {
return fmt.Errorf("failed to process user data: %w", err)
}
return nil
}
Code language: JavaScript (javascript)
Error Handling Patterns
The Sentinel Error Pattern
Define specific error variables for common errors that might need special handling:
var (
ErrNotFound = errors.New("item not found")
ErrInvalidInput = errors.New("invalid input")
)
func findItem(id string) (*Item, error) {
if id == "" {
return nil, ErrInvalidInput
}
// ... implementation
return nil, ErrNotFound
}
Code language: PHP (php)
The Multiple Return Pattern
When a function can fail in multiple ways, return both the result and error:
func divideAndRound(a, b float64) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
result := a / b
rounded := int(math.Round(result))
return rounded, nil
}
Code language: PHP (php)
Testing Error Handling
Don’t forget to test your error handling code:
func TestDivideAndRound(t *testing.T) {
tests := []struct {
name string
a, b float64
want int
wantErr bool
}{
{"valid division", 10, 2, 5, false},
{"division by zero", 10, 0, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := divideAndRound(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("divideAndRound() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && got != tt.want {
t.Errorf("divideAndRound() = %v, want %v", got, tt.want)
}
})
}
}
Code language: JavaScript (javascript)
By following these patterns and best practices, you’ll write more reliable Go code that gracefully handles errors and provides clear feedback when things go wrong. Remember that good error handling is about making your code more maintainable and your application more robust.
Ready to put these concepts into practice? Try refactoring an existing piece of code to implement these error handling patterns, or build a small project that focuses on proper error handling. The more you practice, the more natural these patterns will become.