The early return pattern keeps code clean and reduces nesting:
func process() error {
if err := step1(); err != nil {
return err
}
if err := step2(); err != nil {
return err
}
return step3()
}
The early return pattern has become the preferred approach in Go because it significantly improves code readability and maintainability. By handling errors immediately and returning, you avoid deep nesting that can make code difficult to follow. Instead of wrapping your entire function body in an if-else block, each error check becomes a simple guard clause that exits early when something goes wrong.
This approach creates clear error paths that are easy to trace. Errors are handled at the point they occur, making it obvious what went wrong and where. The pattern also embodies the "fail fast" principle: problems are detected and reported immediately, rather than allowing execution to continue with invalid state. This makes debugging easier and prevents cascading failures that can obscure the root cause of problems.
Add logging when handling errors:
result, err := doSomething()
if err != nil {
log.Printf("Error doing something: %v", err)
return err
}
Deferred functions can return errors, but these errors are typically ignored because there's no way to return them from the deferring function. However, there are patterns for handling errors from deferred operations.
Pattern 1: Logging deferred errors
The most common approach is to log errors from deferred operations:
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
if err := file.Close(); err != nil {
log.Printf("Error closing file: %v", err)
}
}()
// Process file...
return nil
}
Pattern 2: Named return values for deferred errors
If you need to return an error from a deferred operation, use named return values:
func processFile(filename string) (err error) {
file, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
closeErr := file.Close()
if err == nil {
err = closeErr // Only return close error if no other error occurred
}
}()
// Process file...
return nil
}
Pattern 3: Separate error tracking
For more complex scenarios, you might track multiple errors:
func processWithCleanup() (err error) {
var cleanupErr error
defer func() {
if cleanupErr != nil {
log.Printf("Cleanup error: %v", cleanupErr)
if err == nil {
err = cleanupErr
}
}
}()
// Main logic...
return nil
}
The key principle is that if a deferred operation can fail, you should handle that failure appropriately—either by logging it or, if it's critical, by using named return values to propagate it.
Handling errors in loops requires careful consideration. You have several options depending on your use case:
Pattern 1: Stop on first error
If any error should stop processing:
func processItems(items []string) error {
for _, item := range items {
if err := processItem(item); err != nil {
return fmt.Errorf("failed to process %q: %w", item, err)
}
}
return nil
}
Pattern 2: Collect all errors
If you want to process all items and report all errors:
func processItems(items []string) error {
var errs []error
for _, item := range items {
if err := processItem(item); err != nil {
errs = append(errs, fmt.Errorf("item %q: %w", item, err))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
Pattern 3: Log and continue
If errors should be logged but not stop processing:
func processItems(items []string) {
for _, item := range items {
if err := processItem(item); err != nil {
log.Printf("Error processing %q: %v", item, err)
continue
}
}
}
Pattern 4: Partial success tracking
If you need to track which items succeeded and which failed:
func processItems(items []string) (successful []string, failed []error) {
for _, item := range items {
if err := processItem(item); err != nil {
failed = append(failed, fmt.Errorf("item %q: %w", item, err))
} else {
successful = append(successful, item)
}
}
return successful, failed
}
The choice of pattern depends on your requirements: whether errors should stop processing, whether you need to report all errors, or whether processing should continue despite errors.
You can ignore errors using the blank identifier, but this is generally not recommended:
result, _ := doSomething() // Ignore error (not recommended)
When ignoring errors might be acceptable
Complete example
package main
import (
"errors"
"fmt"
)
func readConfig() (string, error) {
return "", errors.New("config file not found")
}
func connectDB() error {
return errors.New("connection failed")
}
func startServer() error {
config, err := readConfig()
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
fmt.Println("Config:", config)
if err := connectDB(); err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
fmt.Println("Server started")
return nil
}
func main() {
if err := startServer(); err != nil {
fmt.Printf("Failed to start server: %v\n", err)
}
}