Golang by Example

Logo

Error Handling

Error handling is the process of responding to and resolving error conditions in a program. In Go, error handling is done using the error type, which is a built-in interface that represents an error condition. The error type is a simple interface that has a single method, Error() string, which returns a string representation of the error. When a function returns an error, it is the caller's responsibility to check for the error and handle it appropriately.

The code above demonstrates how to handle errors in Go. The mayReturnError function returns an error if the shouldError parameter is true. In the main function, we check for the error using an if statement and print the error message if it exists.

Panic 🫨

Panic is a built-in function in Go that stops the normal execution of a goroutine. When a panic occurs, the program will print the panic message and the stack trace, and then it will terminate. Panic is typically used for unrecoverable errors, such as accessing a nil pointer or an out-of-bounds array index.

If we don't check for errors, we may end up with unexpected behavior or panic in our program. For example, if we try to access a nil pointer or divide by zero, our program will panic and crash.

Error type

Compiled from Go Error Handling, now we move into the error type. In Go, errors are represented by the error interface type. Any value that implements this interface can be used as an error, as long as it can provide a string description of itself. The definition of this interface is quite simple:

type error interface {
    Error() string
}

Like other built-in types in Go, the error interface is predefined in the universe block, making it available throughout your code without imports. The errors package provides the most widely used implementation of this interface through its internal errorString type.

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

The errors.New function provides a way to create an error value by converting a provided string into an errors.errorString instance, which is then returned as an error interface.

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

Here’s how you might use errors.New:

When you call Sqrt with a negative number, you receive a non-nil error (specifically an errors.errorString instance). This error message ("math: square root of...") can be retrieved either by invoking the error's Error method or simply printing the error directly:


    fmt.Println(Sqrt(-2.0))

Real world example

Assume you're working on an API that connects to a database. You do multiple operations, such as creating, reading, updating, and deleting records. Each of these operations can fail for various reasons, such as network issues or invalid input. Most of the time this operations runs one after another. So you need to handle errors in a way that allows you abort the operation if one of them fails because your data integrity is at stake.

// UpdateStock updates stock for multiple products
// in a transaction
func (sm *StockManager) UpdateStock(orderItems map[int]int) error {
  // Begin transaction
  tx, err := db.Begin()
  if err != nil {
    return fmt.Errorf("failed to begin transaction: %w", err)
  }
  
  // If function returns with error, rollback the transaction
  // Otherwise, commit the transaction
  defer func() {
    if err != nil {
      tx.Rollback()
      log.Println("Transaction rolled back")
    }
  }()

  // Check and update stock for each product in the order
  for productID, quantity := range orderItems {
    // First check if we have enough stock
    var currentStock int
    err = tx.QueryRow(`
      SELECT
          stock
      FROM
          products
      WHERE
          id = ?
    `, productID).Scan(&currentStock)
    if err != nil {
      return fmt.Errorf("error checking stock for product %d: %w",
           productID, err)
    }
    
    if currentStock < quantity {
      return fmt.Errorf("insufficient stock for product %d", productID)
    }
    
    // Update the stock
    _, err = tx.Exec(`
      UPDATE
          products
      SET
          stock = stock - ?
      WHERE
          id = ?
    `, quantity, productID)
    if err != nil {
      return fmt.Errorf("error updating stock for product %d: %w",
          productID, err)
    }
    
    log.Printf("Updated stock for product %d: %d → %d\n", 
      productID, currentStock, currentStock-quantity)
  }
  
  // If we reach here, all operations succeeded, so commit
  // the transaction
  if err := tx.Commit(); err != nil {
    return fmt.Errorf("failed to commit transaction: %w", err)
  }
  
  log.Println("Transaction committed successfully")
  return nil
}

In this example, we use a transaction to ensure that all stock updates are atomic. If any operation fails, we roll back the transaction to maintain data integrity. The defer statement ensures that the rollback occurs if an error is encountered. More about this, read it in the Execute Transactions article.

Quiz time


package main

import (
    "errors"
    "fmt"
)

func main() {
    var err error // err is nil
    
    if err != nil {
        fmt.Println("Error:", err)
    }
    
    if err := errors.New("oh no!"); err != nil {
        // This creates a new 'err' variable that shadows the outer one
        fmt.Println("Error:", err)
    }
    
    if err == nil {
        fmt.Println("No error!")
    } else {
        fmt.Println("Error:", err)
    }
}
💡 Quiz
What happens when the code above is executed?
💡 Quiz
What is the purpose of the panic function in Go?
💡 Quiz
How do you create a new error in Go?

Wrap up

Error handling is a crucial aspect of programming in Go. By using the error type and the panic function, you can effectively manage errors and ensure that your programs behave predictably. Remember to always check for errors and handle them appropriately to avoid unexpected behavior or crashes in your applications.


Table of contents

Hello World - Begin with the classic Hello World program
Primitives - Learn about the basic data types in Go
Flow Control - Controlling the flow of your program
Struct - All about struct
Functions - Define and call functions
Methods and Interfaces - Methods and interfaces in Go
Error Handling - Handling errors idiomatically
Concurrency - Goroutines and channels
Anti Patterns - Anti patterns in Go
Libraries - Standard library and third-party libraries
Testing - Writing unit tests with `go test`
Benchmarking - Performance benchmarks
Containerization - Dockerize your Go app
What's Next? - Explore the next steps in your Golang journey


Share to: