Skip to main content

Go Error Handling

Error handling is a fundamental aspect of writing robust Go applications. Unlike many other programming languages that use exceptions, Go takes a different approach by treating errors as values that can be returned from functions and handled explicitly. This design philosophy leads to more predictable error handling and forces developers to think about error cases.

Introduction to Error Handling in Go

In Go, functions that can fail often return an error as their last return value. This error is either nil (indicating success) or an error value (indicating failure). This pattern encourages checking errors immediately after a function call.

Let's explore how Go's error handling works and how to implement it effectively in your Gin applications.

The Error Interface

In Go, errors are represented by the built-in error interface:

go
type error interface {
Error() string
}

Any type that implements the Error() method satisfies this interface and can be used as an error.

Basic Error Handling

Here's a simple example of error handling in Go:

go
package main

import (
"fmt"
"errors"
)

func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)

result, err = divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}

Output:

Result: 5
Error: division by zero

In this example:

  1. The divide function returns two values: the result of division and an error.
  2. When we call divide(10, 2), it returns 5, nil (successful operation).
  3. When we call divide(10, 0), it returns 0, errors.New("division by zero") (error).
  4. We check for errors using the if err != nil pattern, which is idiomatic in Go.

Creating Errors

Go provides several ways to create errors:

1. Using errors.New()

go
import "errors"

err := errors.New("something went wrong")

2. Using fmt.Errorf() for formatted error messages

go
import "fmt"

err := fmt.Errorf("invalid value: %d", value)

3. Custom error types

go
type ValidationError struct {
Field string
Message string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("Validation error on field %s: %s", e.Field, e.Message)
}

// Using the custom error
err := &ValidationError{Field: "age", Message: "must be positive"}

Error Handling Patterns

The if-error Pattern

The most common pattern in Go is checking errors immediately after function calls:

go
data, err := readFile("config.json")
if err != nil {
// handle error
return err // or log it, or continue with a default
}
// proceed with data

Wrapping Errors

Go 1.13 introduced error wrapping to add context to errors while preserving the original error:

go
import "fmt"

func processFile(path string) error {
data, err := readFile(path)
if err != nil {
return fmt.Errorf("failed to process file %s: %w", path, err)
}
// process data
return nil
}

The %w verb wraps the original error so it can be retrieved later using errors.Unwrap() or detected with errors.Is().

Checking Error Types

Go provides functions to check error types:

go
// Check if an error is a specific error value
if errors.Is(err, io.EOF) {
// handle EOF
}

// Check if an error is of a specific type
var validationErr *ValidationError
if errors.As(err, &validationErr) {
// handle validation error specifically
fmt.Println(validationErr.Field, validationErr.Message)
}

Error Handling in Gin Applications

In Gin web framework applications, effective error handling is crucial for building robust APIs. Let's look at how to handle errors in Gin:

Basic Error Handling

go
func GetUser(c *gin.Context) {
id := c.Param("id")

user, err := db.GetUser(id)
if err != nil {
// Check for specific error types
if errors.Is(err, db.ErrUserNotFound) {
c.JSON(http.StatusNotFound, gin.H{
"error": "User not found",
})
return
}

// Default error handling
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to retrieve user",
})
return
}

c.JSON(http.StatusOK, user)
}

Custom Error Middleware

In Gin, you can create middleware to handle errors consistently across your application:

go
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
// Process request
c.Next()

// Check if there were any errors
if len(c.Errors) > 0 {
// Get the last error
err := c.Errors.Last()

// Handle different types of errors
var statusCode int
var response gin.H

var validationErr *ValidationError
if errors.As(err.Err, &validationErr) {
statusCode = http.StatusBadRequest
response = gin.H{
"error": "Validation failed",
"field": validationErr.Field,
"message": validationErr.Message,
}
} else {
statusCode = http.StatusInternalServerError
response = gin.H{
"error": "Internal server error",
}
}

c.JSON(statusCode, response)
c.Abort()
}
}
}

Using this middleware in your Gin application:

go
func main() {
router := gin.New()

// Apply the error handler middleware
router.Use(ErrorHandler())

// Routes
router.GET("/users/:id", GetUser)

router.Run(":8080")
}

Adding Errors in Gin Handlers

In your handler functions, you can add errors to the context:

go
func CreateUser(c *gin.Context) {
var user User

if err := c.ShouldBindJSON(&user); err != nil {
c.Error(&ValidationError{Field: "request body", Message: "invalid format"})
return
}

if user.Age < 0 {
c.Error(&ValidationError{Field: "age", Message: "must be positive"})
return
}

// Proceed with user creation if no errors
// ...

c.JSON(http.StatusCreated, gin.H{"id": user.ID})
}

Best Practices for Error Handling

  1. Always check errors - Don't ignore returned errors.

  2. Be specific - Create or use error types that provide enough information.

  3. Add context - When returning errors up the call stack, add context about where and why the error occurred.

  4. Don't overuse panics - In Go, panics are for unrecoverable situations, not for regular error handling.

  5. Log appropriately - Consider which errors need to be logged and at what level.

  6. Return useful error messages - Error messages should be clear and actionable.

  7. Consider the consumer - When building APIs, return appropriate HTTP status codes and structured error responses.

Real-world Example: REST API with Error Handling

Let's build a simple REST API for a product service with comprehensive error handling:

go
package main

import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)

// Error types
var (
ErrProductNotFound = errors.New("product not found")
ErrInvalidProduct = errors.New("invalid product data")
ErrDatabaseError = errors.New("database error")
)

// Custom domain-specific error
type AppError struct {
Err error
Message string
HTTPCode int
}

func (e *AppError) Error() string {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}

// Product model
type Product struct {
ID string `json:"id"`
Name string `json:"name" binding:"required"`
Price float64 `json:"price" binding:"required,gt=0"`
}

// Mock database
var products = map[string]Product{
"1": {ID: "1", Name: "Go Programming Book", Price: 29.99},
}

// Database access methods with error handling
func getProduct(id string) (Product, error) {
product, exists := products[id]
if !exists {
return Product{}, ErrProductNotFound
}
return product, nil
}

func createProduct(p Product) error {
if p.Name == "" || p.Price <= 0 {
return ErrInvalidProduct
}

// Simulate potential database errors (10% chance)
/*
if rand.Float32() < 0.1 {
return ErrDatabaseError
}
*/

products[p.ID] = p
return nil
}

// Error handling middleware
func ErrorMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()

if len(c.Errors) > 0 {
err := c.Errors.Last()

// Check for custom app errors
var appErr *AppError
if errors.As(err.Err, &appErr) {
c.JSON(appErr.HTTPCode, gin.H{
"error": appErr.Message,
})
return
}

// Handle standard errors
var statusCode int
var message string

switch {
case errors.Is(err.Err, ErrProductNotFound):
statusCode = http.StatusNotFound
message = "Product not found"
case errors.Is(err.Err, ErrInvalidProduct):
statusCode = http.StatusBadRequest
message = "Invalid product data"
default:
statusCode = http.StatusInternalServerError
message = "Internal server error"
}

c.JSON(statusCode, gin.H{
"error": message,
})
}
}
}

// Handler functions
func getProductHandler(c *gin.Context) {
id := c.Param("id")

product, err := getProduct(id)
if err != nil {
c.Error(err)
return
}

c.JSON(http.StatusOK, product)
}

func createProductHandler(c *gin.Context) {
var product Product

// Bind JSON and validate
if err := c.ShouldBindJSON(&product); err != nil {
appErr := &AppError{
Err: err,
Message: "Invalid product format",
HTTPCode: http.StatusBadRequest,
}
c.Error(appErr)
return
}

// Generate a simple ID (in production, use UUID or similar)
product.ID = fmt.Sprintf("%d", len(products) + 1)

// Create the product
if err := createProduct(product); err != nil {
c.Error(err)
return
}

c.JSON(http.StatusCreated, product)
}

func main() {
router := gin.Default()

// Apply error handling middleware
router.Use(ErrorMiddleware())

// Routes
router.GET("/products/:id", getProductHandler)
router.POST("/products", createProductHandler)

router.Run(":8080")
}

You can test this API with:

bash
# Get an existing product
curl http://localhost:8080/products/1

# Get a non-existent product
curl http://localhost:8080/products/999

# Create a valid product
curl -X POST http://localhost:8080/products \
-H "Content-Type: application/json" \
-d '{"name":"Go Programming Advanced","price":39.99}'

# Create an invalid product
curl -X POST http://localhost:8080/products \
-H "Content-Type: application/json" \
-d '{"name":"","price":-10}'

Summary

Go's approach to error handling is explicit and straightforward, treating errors as regular values that can be checked, propagated, and handled. This design encourages developers to consider error cases and handle them appropriately.

Key points to remember:

  • Errors in Go are values, not exceptions
  • Functions typically return errors as their last return value
  • Use the if err != nil pattern to check for errors
  • Create custom error types for domain-specific errors
  • Use error wrapping to add context while preserving the original error
  • In Gin applications, use middleware for consistent error handling
  • Return appropriate HTTP status codes and structured error responses

By following these principles, you'll write more robust Go applications with clear error handling patterns.

Additional Resources

Exercises

  1. Create a custom error type for validation errors that includes field name, value, and reason for failure.
  2. Implement a Gin middleware that logs all errors to a file before responding to the client.
  3. Build a simple REST API with at least three endpoints, implementing proper error handling for each.
  4. Modify the product service example to include database operations and handle different types of database errors.
  5. Implement retry logic for operations that might fail temporarily (like external API calls).


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)