Error Handling in Go
Go handles errors differently from many other languages. Instead of using exceptions, Go uses explicit error handling where functions that can fail return an error value as their last return value.
The Error Interface
In Go, errors are values that implement the built-in error interface:
Error Interface Definition
type error interface {
Error() string
}Basic Error Handling
Simple Error Example
package main
import (
"errors"
"fmt"
"strconv"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide 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) // Output: Result: 5
result, err = divide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Output: Error: cannot divide by zero
return
}
fmt.Println("Result:", result)
// Example with strconv
str := "123"
num, err := strconv.Atoi(str)
if err != nil {
fmt.Println("Error converting string to int:", err)
return
}
fmt.Println("Converted number:", num)
str2 := "abc"
num2, err := strconv.Atoi(str2)
if err != nil {
fmt.Println("Error converting string to int:", err)
// Output: Error converting string to int: strconv.Atoi: parsing "abc": invalid syntax
return
}
fmt.Println("Converted number:", num2)
}Creating Custom Errors
package main
import "fmt"
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (ve ValidationError) Error() string {
return fmt.Sprintf("validation error for field '%s': %s", ve.Field, ve.Message)
}
type User struct {
Name string
Email string
Age int
}
func (u User) Validate() error {
if u.Name == "" {
return ValidationError{Field: "name", Message: "name cannot be empty"}
}
if u.Email == "" {
return ValidationError{Field: "email", Message: "email cannot be empty"}
}
if u.Age < 0 || u.Age > 120 {
return ValidationError{Field: "age", Message: "age must be between 0 and 120"}
}
return nil
}
func main() {
users := []User{
{Name: "John", Email: "[email protected]", Age: 30},
{Name: "", Email: "[email protected]", Age: 25},
{Name: "Bob", Email: "[email protected]", Age: -5},
}
for i, user := range users {
fmt.Printf("Validating user %d...\n", i+1)
if err := user.Validate(); err != nil {
fmt.Println("Validation failed:", err)
} else {
fmt.Println("Validation passed!")
}
fmt.Println()
}
}Error Wrapping with fmt.Errorf
Go 1.13 introduced error wrapping with %w verb:
package main
import (
"errors"
"fmt"
)
func processFile(filename string) error {
// Simulate a file operation error
return fmt.Errorf("failed to process file '%s': %w", filename,
errors.New("permission denied"))
}
func main() {
err := processFile("/etc/config.yaml")
if err != nil {
fmt.Println("Error:", err)
// Check if the error contains a specific wrapped error
if errors.Is(err, errors.New("permission denied")) {
fmt.Println("This is a permission error!")
}
}
}Error Types and Type Assertions
Working with Different Error Types
package main
import (
"fmt"
"net"
"os"
)
type NetworkError struct {
Op string
Net string
Addr string
Err error
}
func (ne NetworkError) Error() string {
return fmt.Sprintf("network error: %s %s %s: %v", ne.Op, ne.Net, ne.Addr, ne.Err)
}
func (ne NetworkError) Unwrap() error {
return ne.Err
}
func connectToServer(address string) error {
conn, err := net.Dial("tcp", address)
if err != nil {
return NetworkError{
Op: "dial",
Net: "tcp",
Addr: address,
Err: err,
}
}
defer conn.Close()
fmt.Println("Connected successfully!")
return nil
}
func main() {
// Try to connect to a non-existent server
err := connectToServer("localhost:9999")
if err != nil {
fmt.Println("Connection error:", err)
// Type assertion to get the specific error type
if netErr, ok := err.(NetworkError); ok {
fmt.Printf("Operation: %s\n", netErr.Op)
fmt.Printf("Address: %s\n", netErr.Addr)
fmt.Printf("Underlying error: %v\n", netErr.Err)
}
// Check for specific underlying errors
if errors.Is(err, os.ErrPermission) {
fmt.Println("This is a permission error")
}
}
}Best Practices for Error Handling
1. Always Handle Errors
// Bad: Ignoring errors
file, _ := os.Open("config.txt")
data, _ := ioutil.ReadAll(file)
// Good: Handling errors
file, err := os.Open("config.txt")
if err != nil {
log.Printf("Failed to open config file: %v", err)
return
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
log.Printf("Failed to read config file: %v", err)
return
}2. Provide Context in Errors
// Bad: Vague error
func processData(data []byte) error {
if len(data) == 0 {
return errors.New("invalid data")
}
// ... process data
}
// Good: Specific error with context
func processData(data []byte) error {
if len(data) == 0 {
return fmt.Errorf("processData: data slice is empty")
}
// ... process data
}3. Use Sentinel Errors for Known Conditions
package main
import (
"errors"
"fmt"
)
// Sentinel errors
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidPassword = errors.New("invalid password")
ErrAccountLocked = errors.New("account is locked")
)
type User struct {
ID string
Username string
Password string
Locked bool
}
func authenticate(username, password string) (*User, error) {
users := map[string]User{
"alice": {ID: "1", Username: "alice", Password: "secret123", Locked: false},
"bob": {ID: "2", Username: "bob", Password: "password", Locked: true},
}
user, exists := users[username]
if !exists {
return nil, ErrUserNotFound
}
if user.Locked {
return nil, ErrAccountLocked
}
if user.Password != password {
return nil, ErrInvalidPassword
}
return &user, nil
}
func main() {
testCases := []struct {
username string
password string
}{
{"alice", "secret123"},
{"bob", "password"},
{"charlie", "nopass"},
{"alice", "wrongpass"},
}
for _, tc := range testCases {
fmt.Printf("Testing login for %s...\n", tc.username)
user, err := authenticate(tc.username, tc.password)
if err != nil {
switch {
case errors.Is(err, ErrUserNotFound):
fmt.Printf(" User not found\n")
case errors.Is(err, ErrInvalidPassword):
fmt.Printf(" Invalid password\n")
case errors.Is(err, ErrAccountLocked):
fmt.Printf(" Account is locked\n")
default:
fmt.Printf(" Unknown error: %v\n", err)
}
} else {
fmt.Printf(" Login successful! User ID: %s\n", user.ID)
}
fmt.Println()
}
}Panic and Recover
While Go encourages explicit error handling, sometimes you need to handle unrecoverable situations with panic.
Using Panic
package main
import (
"fmt"
"os"
)
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
panic(fmt.Sprintf("Failed to open file %s: %v", filename, err))
}
defer file.Close()
fmt.Printf("Successfully opened %s\n", filename)
}
func main() {
fmt.Println("Starting program...")
// This will panic if the file doesn't exist
readFile("nonexistent.txt")
fmt.Println("This line won't be reached")
}Using Recover
package main
import (
"fmt"
"log"
)
func riskyOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
}
}()
fmt.Println("Starting risky operation...")
// Simulate a panic
panic("something went wrong!")
fmt.Println("This won't be printed")
}
func safeOperation() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic caught in safeOperation: %v", r)
}
}()
riskyOperation()
fmt.Println("This will be printed because we recovered")
}
func main() {
fmt.Println("Main program starting...")
safeOperation()
fmt.Println("Main program continues...")
}Practical Error Handling Examples
File Processing with Error Handling
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
type Config struct {
Port int
Host string
Debug bool
LogLevel string
}
func loadConfig(filename string) (*Config, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("loadConfig: failed to open file %s: %w", filename, err)
}
defer file.Close()
config := &Config{}
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("loadConfig: invalid format on line %d: %s", lineNum, line)
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch key {
case "port":
port, err := strconv.Atoi(value)
if err != nil {
return nil, fmt.Errorf("loadConfig: invalid port value on line %d: %w", lineNum, err)
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("loadConfig: port must be between 1 and 65535, got %d on line %d", port, lineNum)
}
config.Port = port
case "host":
config.Host = value
case "debug":
debug, err := strconv.ParseBool(value)
if err != nil {
return nil, fmt.Errorf("loadConfig: invalid debug value on line %d: %w", lineNum, err)
}
config.Debug = debug
case "log_level":
validLevels := []string{"debug", "info", "warn", "error"}
valid := false
for _, level := range validLevels {
if value == level {
valid = true
break
}
}
if !valid {
return nil, fmt.Errorf("loadConfig: invalid log level '%s' on line %d", value, lineNum)
}
config.LogLevel = value
default:
return nil, fmt.Errorf("loadConfig: unknown configuration key '%s' on line %d", key, lineNum)
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("loadConfig: error reading file: %w", err)
}
// Set default values
if config.Port == 0 {
config.Port = 8080
}
if config.Host == "" {
config.Host = "localhost"
}
if config.LogLevel == "" {
config.LogLevel = "info"
}
return config, nil
}
func main() {
// Create a sample config file
configContent := `# Server Configuration
port=8080
host=localhost
debug=true
log_level=info
`
err := os.WriteFile("config.txt", []byte(configContent), 0644)
if err != nil {
fmt.Printf("Failed to create config file: %v\n", err)
return
}
// Load the configuration
config, err := loadConfig("config.txt")
if err != nil {
fmt.Printf("Error loading configuration: %v\n", err)
return
}
fmt.Printf("Configuration loaded successfully:\n")
fmt.Printf(" Host: %s\n", config.Host)
fmt.Printf(" Port: %d\n", config.Port)
fmt.Printf(" Debug: %t\n", config.Debug)
fmt.Printf(" Log Level: %s\n", config.LogLevel)
// Clean up
os.Remove("config.txt")
}HTTP Server Error Handling
package main
import (
"errors"
"fmt"
"net/http"
)
type APIError struct {
Code int
Message string
Details string
}
func (ae APIError) Error() string {
return fmt.Sprintf("API error %d: %s - %s", ae.Code, ae.Message, ae.Details)
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var users = map[string]User{
"1": {ID: "1", Name: "John Doe", Email: "[email protected]"},
"2": {ID: "2", Name: "Jane Smith", Email: "[email protected]"},
}
func getUser(id string) (*User, error) {
if id == "" {
return nil, APIError{
Code: http.StatusBadRequest,
Message: "missing user ID",
Details: "User ID is required in the URL path",
}
}
user, exists := users[id]
if !exists {
return nil, APIError{
Code: http.StatusNotFound,
Message: "user not found",
Details: fmt.Sprintf("User with ID %s does not exist", id),
}
}
return &user, nil
}
func userHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
user, err := getUser(id)
if err != nil {
// Type assertion to check if it's an APIError
if apiErr, ok := err.(APIError); ok {
http.Error(w, apiErr.Message, apiErr.Code)
return
}
// Handle other types of errors
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "User found: %s (%s)", user.Name, user.Email)
}
func main() {
http.HandleFunc("/user", userHandler)
fmt.Println("Server starting on :8080...")
fmt.Println("Test with:")
fmt.Println(" curl 'http://localhost:8080/user?id=1'")
fmt.Println(" curl 'http://localhost:8080/user?id=999'")
fmt.Println(" curl 'http://localhost:8080/user'")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Printf("Server failed to start: %v\n", err)
}
}Error Handling Best Practices Summary
- Always handle errors returned by functions
- Return errors as the last return value
- Use sentinel errors for known error conditions
- Add context to errors using
fmt.Errorfwith%w - Create custom error types for complex error scenarios
- Use
errors.Isanderrors.Asfor error comparison and type checking - Avoid panicking unless you have an unrecoverable error
- Use
deferwithrecoversparingly and only when necessary - Be consistent in your error handling approach
- Document your errors so callers know what to expect
External Resources:
Related Tutorials:
Last updated on