Error Handling in Go!

Gaurav Kapatia
Newton School
Published in
3 min readJul 15, 2024

In Go, error handling is an important aspect of the language’s design. Unlike languages that use exceptions for error handling, Go uses a combination of multiple return values and a built-in error type. This approach emphasises explicit error checking and handling.

Here’s a detailed explanation of error handling in Go:

The error Type

Go has a built-in interface type called error. It is defined as:

type error interface {
Error() string
}

Any type that implements the Error() method with the signature Error() string is considered to be of type error.

Returning Errors

Functions in Go often return an error as the last return value. Here’s an example of a function that returns an error:

package main

import (
"errors"
"fmt"
)

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(4, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}

In this example, the divide function checks if b is zero. If it is, the function returns an error using errors.New(). Otherwise, it returns the result of the division and nil for the error.

Custom Errors

We can define custom error types by implementing the Error() method:

package main

import (
"fmt"
)

type MyError struct {
Code int
Message string
}

func (e *MyError) Error() string {
return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

func doSomething() error {
return &MyError{Code: 123, Message: "Something went wrong"}
}

func main() {
err := doSomething()
if err != nil {
fmt.Println(err)
}
}

Here, MyError is a custom error type that includes an error code and a message. The doSomething function returns an instance of MyError.

Wrapping and Unwrapping Errors

Go 1.13 introduced error wrapping. We can wrap errors with additional context using fmt.Errorf:

package main

import (
"fmt"
"errors"
)

func doSomething() error {
return errors.New("original error")
}

func main() {
err := doSomething()
if err != nil {
wrappedErr := fmt.Errorf("additional context: %w", err)
fmt.Println(wrappedErr) // prints "additional context: original error"
}
}

To unwrap an error and retrieve the original error, we can use the errors.Unwrap function:

package main

import (
"errors"
"fmt"
)

func doSomething() error {
return errors.New("original error")
}

func main() {
originalErr := doSomething()
if originalErr != nil {
wrappedErr := fmt.Errorf("additional context: %w", originalErr)

unwrappedErr := errors.Unwrap(wrappedErr)
fmt.Println(unwrappedErr) // prints "original error"
}
}

Error Handling Best Practices

  1. Check Errors Explicitly: Always check the returned error explicitly and handle it accordingly.
  2. Return Early: Use early returns to simplify the control flow when an error occurs.
  3. Add Context: When returning an error, add context to help diagnose the issue.
  4. Use Custom Errors: Define custom error types for more complex error scenarios.

Example with Error Handling

Here’s an example of a more complex function with proper error handling:

package main

import (
"fmt"
"os"
)

func readFile(fileName string) ([]byte, error) {
file, err := os.Open(fileName)
if err != nil {
return nil, fmt.Errorf("failed to open file %s: %w", fileName, err)
}
defer file.Close()

stat, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}

if stat.Size() == 0 {
return nil, fmt.Errorf("file %s is empty", fileName)
}

data := make([]byte, stat.Size())
_, err = file.Read(data)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}

return data, nil
}

func main() {
data, err := readFile("test.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("File Content:", string(data))
}

In this example, readFile handles various error scenarios: opening the file, getting file information, checking if the file is empty, and reading the file content. Each error is wrapped with context to provide more useful error messages.

By following these practices, we can write robust and maintainable error handling code in Go.

--

--

Gaurav Kapatia
Newton School

Tech Lead at Newton School. Python, Django, Kubernetes entusiast. Leading tech at a $100M+ ed-tech startup. Passionate about innovation and scalable solutions.