Open In App

Go Error Handling

Last Updated : 04 Feb, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

In Go, error handling is done by returning error values instead of using try-catch like in Java or Python. This approach ensures explicit error handling, improving code clarity and control.

The error type in Go is an interface with a single method:

type error interface {
Error() string
}

Any type implementing this method is treated as an error, keeping error handling simple and avoiding complex exception hierarchies.

Creating and Returning Errors in Go

1. Using errors.New

Go provides a simple errors package for creating basic error messages. Here's how you can create and return an error using errors.New:

Go
package main

import (
    "errors"
    "fmt"
)

// checkNumber function checks if a number is positive or negative.
// If the number is negative, it returns an error.
func checkNumber(num int) (string, error) {
    if num < 0 {
        // Return an error when the number is negative
        return "", errors.New("number is negative")
    }
    // Return a success message if the number is positive
    return "number is positive", nil
}

func main() {
    // Calling checkNumber with a negative value (-5)
    result, err := checkNumber(-5)

    if err != nil {
        // If an error is returned, print the error message
        fmt.Println("Error:", err) // Output: Error: number is negative
    } else {
        // If no error, print the success message
        fmt.Println(result)
    }
}

Output
Error: number is negative

Explanation:

  • The checkNumber function returns an error if the number is negative.
  • If no error occurs, the function returns a success message along with a nil error.

2. Returning nil for No Error

When no error occurs, it's common to return nil as the error value. This is the default "zero" value for errors in Go:

return "operation successful", nil

Wrapping Errors for Better Traceability

For more complex error handling, Go provides fmt.Errorf to wrap errors with additional context. This feature is incredibly useful for tracing the source of errors and understanding their context as they bubble up through function calls.

Example:

Go
package main

import (
    "errors"
    "fmt"
)

func checkNumber(num int) (string, error) {
    if num < 0 {
        return "", errors.New("number is negative")
    }
    return "number is positive", nil
}

func main() {
    result, err := checkNumber(-5)
    if err != nil {
        fmt.Println("Error:", err) // Output: Error: number is negative
    } else {
        fmt.Println(result)
    }
}

Output
Error: number is negative

The %w verb in fmt.Errorf can wrap an error to preserve its context for further investigation:

err := fmt.Errorf("failed operation: %w", errors.New("network timeout"))
originalErr := errors.Unwrap(err)
fmt.Println(originalErr) // Output: network timeout

Unwrapping Errors

In Go 1.13 and later, the errors package provides the Unwrap function to extract the original error from a wrapped error. This is particularly useful when multiple layers of errors are involved.

Example:

err := fmt.Errorf("failed operation: %w", errors.New("network timeout"))
originalErr := errors.Unwrap(err)
fmt.Println(originalErr) // Output: network timeout

Comparing Errors with errors.Is

Go provides the errors.Is function to compare errors, including errors that have been wrapped. This allows you to check if an error matches a specific predefined error, even if it has been wrapped multiple times.

Example:

Go
import (
    "errors"
    "fmt"
)

// Declaring a custom error for invalid input
var ErrInvalidInput = errors.New("invalid input")

// Function to validate input and return an error if the input is empty
func validateInput(input string) error {
    if input == "" {
        // Wrapping the custom error using fmt.Errorf and %w for proper error chaining
        return fmt.Errorf("validation error: %w", ErrInvalidInput)
    }
    return nil // No error if the input is valid
}

func main() {
    // Calling validateInput with an empty string to simulate an error
    err := validateInput("")

    // Checking if the returned error matches the custom error using errors.Is()
    if errors.Is(err, ErrInvalidInput) {
        fmt.Println("Detected invalid input") // Prints message if error is of type ErrInvalidInput
    }
}

Explanation:

  • We define a sentinel error ErrInvalidInput.
  • errors.Is checks whether the error returned from validateInput matches the sentinel error, even if it's wrapped in additional context.

Using Custom Errors

While Go’s built-in error handling works for many scenarios, sometimes you need more specific error types. Custom error types can carry additional data and provide more context about the error.

Example:

Go
package main

import (
    "fmt"
)

// CustomError defines a custom error type with a code and message.
type CustomError struct {
    Code    int
    Message string
}

// Error implements the error interface for CustomError.
func (e *CustomError) Error() string {
    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

// generateError creates and returns a new CustomError.
func generateError() error {
    return &CustomError{Code: 404, Message: "Resource not found"}
}

func main() {
    err := generateError()
    fmt.Println(err) // Output: Error 404: Resource not found
}

Explanation:

  • We define a CustomError struct with additional fields for Code and Message.
  • The Error method implements the error interface, allowing it to be used as an error.

Key Error Handling Functions in Go

  • errors.New: Creates a basic error with a message.
  • fmt.Errorf: Wraps an error with additional context, using %w to preserve the original error.
  • errors.Unwrap: Extracts the original error from a wrapped error.
  • errors.Is: Checks if an error matches a predefined error.
  • errors.As: Extracts a specific error type from an error chain.

Best Practices for Error Handling in Go

  • Use Specifications: Define common request and response properties in one place to avoid repetition.
  • Modularize Code: Create reusable methods for commonly used operations, such as creating test data or making common API calls.
  • Validate Response Details: Always verify status codes, response bodies, and headers to ensure correct API behavior.
  • Handle Dynamic Data: Use response data from one API call in subsequent requests to test end-to-end workflows.
  • Incorporate Logging: Enable logs for debugging and detailed visibility during failures.

Conclusion

Go’s error handling is simple and explicit, treating errors as values for better code predictability. You can create errors using errors.New, wrap them with fmt.Errorf, or define custom error types. By following best practices, you can write robust, maintainable, and bug-free code, ensuring smooth development and production performance.


Next Article
Article Tags :

Similar Reads