Error Handling in Go: try/catch and Beyond from A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Error Handling in Go: Philosophy and Practice



Unlike many modern languages, Go (Golang) does not use the classic try/catch construct. Instead, Go developers adhere to the "errors as values" approach. This makes code more explicit, predictable, and safe. In this article, we will cover all aspects of error handling in Go: from basic patterns to advanced techniques with defer, panic, and recover.



1. Basics: Errors Are Just Values



In Go, an error is an interface error with a single method Error() string. Any function that can fail returns an error as its last return value.



package main

import ( "errors" "fmt")

// Division function returning an errorfunc divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero is not allowed") } return a / b, nil}

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


Key point: the if err != nil check is the standard way to handle errors in Go. There are no hidden exceptions that can "pop out" unexpectedly.



2. Creating and Customizing Errors



Go provides several ways to create errors:



  • errors.New() — a simple text error.
  • fmt.Errorf() — a formatted error (supports %w for wrapping).
  • Custom types — for passing additional context.


Custom Error with Additional Data



type DivisionError struct {    Dividend float64    Divisor  float64    Message  string}

func (e *DivisionError) Error() string { return fmt.Sprintf("%s: %f / %f", e.Message, e.Dividend, e.Divisor)}

func safeDivide(a, b float64) (float64, error) { if b == 0 { return 0, &DivisionError{ Dividend: a, Divisor: b, Message: "division by zero", } } return a / b, nil}

func main() { _, err := safeDivide(10, 0) if err != nil { // Type assertion to get additional fields var divErr *DivisionError if errors.As(err, &divErr) { fmt.Printf("Error with %f divided by %f: %s", divErr.Dividend, divErr.Divisor, divErr.Message) } }}


Using errors.As() allows you to check if an error is of a specific type and extract data from it. This is the equivalent of instanceof in other languages.



3. Error Handling Patterns: From Simple to Complex



Go has several established patterns for working with errors:



3.1. Sentinel Errors

These are predefined errors that can be compared using ==. Example: io.EOF.



import (    "errors"    "fmt")

var ErrNotFound = errors.New("element not found")

func findUser(id int) (string, error) { if id != 1 { return "", ErrNotFound } return "Alice", nil}

func main() { _, err := findUser(2) if errors.Is(err, ErrNotFound) { fmt.Println("User not found, creating a new one...") }}


3.2. Error Wrapping

Starting from Go 1.13, you can wrap errors using %w in fmt.Errorf. This preserves the error chain.



func readConfig(path string) error {    data, err := os.ReadFile(path)    if err != nil {        return fmt.Errorf("reading config %s: %w", path, err)    }    // ... processing data                

Blogs

Book Recommendations