- The Dev Loop
- Posts
- The Golang Chronicle #10 – Error Handling in Go
The Golang Chronicle #10 – Error Handling in Go
Idiomatic Patterns & New Approaches

📢 Introduction: Why Error Handling Matters in Go
Error handling is a critical aspect of software development, and in Go, it’s elevated to a design philosophy. Unlike many languages that rely heavily on exceptions, Go embraces explicit error handling, which encourages developers to think critically about how errors are propagated and resolved.
In this edition of The Golang Chronicle, we explore idiomatic error-handling practices in Go, recent updates to error handling, and practical tips for writing robust, maintainable Go applications.
🔧 1. The Basics: Idiomatic Error Handling in Go
Go’s error handling revolves around its simple error
interface:
// The error interface in Go
type error interface {
Error() string
}
The idiomatic pattern involves returning errors from functions and explicitly checking for them:
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
Key Points:
Functions return a value and an
error
.Errors are checked immediately after function calls.
🔌 2. Enhanced Error Handling with errors
Package (Go 1.13+)
Starting with Go 1.13, the errors
package introduced powerful utilities for wrapping and unwrapping errors:
Wrapping Errors with Context
package main
import (
"errors"
"fmt"
)
func readFile(filename string) error {
return fmt.Errorf("failed to read file %s: %w", filename, errors.New("file not found"))
}
func main() {
err := readFile("test.txt")
if err != nil {
fmt.Println(err)
}
}
``: Allows error wrapping for additional context.
``: Extracts the original error.
Checking Error Types
Use errors.Is
and errors.As
to check for specific error types:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func fetchResource(id int) error {
return fmt.Errorf("resource fetch failed: %w", ErrNotFound)
}
func main() {
err := fetchResource(1)
if errors.Is(err, ErrNotFound) {
fmt.Println("Resource not found")
}
}
🚀 3. Custom Error Types for Better Debugging
For more structured error handling, you can define custom error types:
package main
import "fmt"
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validateInput(input string) error {
if input == "" {
return &ValidationError{
Field: "Input",
Message: "cannot be empty",
}
}
return nil
}
func main() {
err := validateInput("")
if err != nil {
fmt.Println(err)
}
}
Benefits:
Custom error types make it easier to categorize and debug errors.
They allow you to include additional context-specific information.
⚙️ 4. Logging and Error Propagation
Instead of logging errors at every level, log them where they are ultimately handled:
package main
import (
"errors"
"fmt"
"log"
)
func service() error {
return errors.New("database connection failed")
}
func handler() error {
return fmt.Errorf("service error: %w", service())
}
func main() {
err := handler()
if err != nil {
log.Fatalf("Critical error: %v", err)
}
}
Tips:
Use structured logging tools like
logrus
orzap
for production systems.Centralize error logging to avoid redundant messages.
⏳ 5. Contextual Error Handling with context
Package
The context
package is essential for managing timeouts and cancellations in concurrent programs:
package main
import (
"context"
"errors"
"fmt"
"time"
)
func longRunningTask(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return errors.New("task completed")
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := longRunningTask(ctx)
if err != nil {
fmt.Println("Error:", err)
}
}
Use Cases:
Canceling goroutines when the work is no longer needed.
Timing out slow operations to prevent blocking.
✨ Best Practices for Error Handling
Return Errors Early: Check for and return errors as soon as they occur.
Wrap Errors for Context: Use
fmt.Errorf
with%w
to provide more context.Avoid Silent Failures: Always handle errors, even if it’s just logging them.
Use Custom Error Types: For complex applications, create meaningful error types.
Leverage Context: Use
context.Context
to manage timeouts and cancellations.
🌟 Conclusion: Writing Resilient Go Programs
Error handling in Go is explicit, predictable, and powerful. By following these idiomatic patterns and leveraging the latest tools, you can create robust applications that are easier to debug and maintain.
💻 Join the GoLang Community!
Join the GoLang Community to discuss Go scheduling, performance optimization, and more with fellow Go enthusiasts.
Go Community: The Dev Loop Community
Cheers,
Aravinth Veeramuthu
The Dev Loop Team