Error Handling

Back

Loading concept...

🚨 Error Handling in Go: Your Safety Net for Code

Imagine you’re a detective 🕵️ solving mysteries. Every time something goes wrong in your program, errors are the clues that help you figure out what happened!


The Big Picture: What Are Errors?

Think of errors like warning signs on a road. When you’re driving, signs tell you “Bridge Out Ahead!” or “Slippery When Wet!” In Go, errors tell your program “Hey, something didn’t work as expected!”

Why is this important?

  • Programs talk to the internet (it might be down!)
  • Programs read files (the file might not exist!)
  • Programs do math (you can’t divide by zero!)

Go helps you handle these problems gracefully instead of crashing like a car without brakes.


🎯 The error Interface

What is it?

In Go, an error is super simple. It’s just anything that can say what went wrong.

type error interface {
    Error() string
}

That’s it! If something has an Error() method that returns a string, it’s an error.

Real-Life Analogy

Imagine every problem in life had to fill out a form with ONE question:

“What went wrong?”

That’s what the error interface does. Any type that can answer this question is an error!

Simple Example

package main

import "fmt"

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

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

Output: Oops: cannot divide by zero


✨ Error Creation

Three Ways to Create Errors

Go gives you multiple tools to create errors. Like having different pens for different jobs!

1️⃣ errors.New() - The Simple Way

import "errors"

err := errors.New("something went wrong")

Use this when you need a basic error message.

2️⃣ fmt.Errorf() - The Smart Way

import "fmt"

name := "config.txt"
err := fmt.Errorf("file not found: %s", name)

Use this when you want to include details in your error.

When to Use Each?

Method Best For
errors.New() Simple, fixed messages
fmt.Errorf() Messages with variables

🎁 Error Wrapping

The Problem

Imagine you’re passing a message in a game of telephone. Each person adds context:

  • Person 1: “The cake fell”
  • Person 2: “While carrying the cake, the cake fell”
  • Person 3: “At the party, while carrying the cake, the cake fell”

That’s error wrapping! You keep the original error but add more context.

How to Wrap Errors

Use fmt.Errorf() with the special %w verb:

originalErr := errors.New("disk full")
wrappedErr := fmt.Errorf("save failed: %w", originalErr)

fmt.Println(wrappedErr)
// Output: save failed: disk full

Why Wrap?

graph TD A["Original: disk full"] --> B["save failed: disk full"] B --> C["backup failed: save failed: disk full"] C --> D["User sees full story!"]

Without wrapping, you’d only know “backup failed” but not WHY!


🔗 errors.Join - Combining Multiple Errors

The Scenario

What if TWO things go wrong at the same time? Like spilling your drink AND dropping your phone?

The Solution

import "errors"

err1 := errors.New("database connection failed")
err2 := errors.New("cache connection failed")

combined := errors.Join(err1, err2)
fmt.Println(combined)

Output:

database connection failed
cache connection failed

Visual

graph TD A["Error 1: DB failed"] --> C["Combined Error"] B["Error 2: Cache failed"] --> C C --> D["Both problems reported!"]

🔍 Error Inspection

Two Superpowers: errors.Is() and errors.As()

errors.Is() - “Is this THE error?”

Like asking: “Is this the SAME person?” even if they’re wearing a disguise (wrapped).

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

err := fmt.Errorf("user lookup: %w", ErrNotFound)

if errors.Is(err, ErrNotFound) {
    fmt.Println("The item doesn't exist!")
}

Even though err is wrapped, errors.Is() can see through the wrapping!

errors.As() - “Can I use this error’s special features?”

Like asking: “Is this person a doctor? If so, let me talk to them AS a doctor.”

var pathErr *os.PathError

if errors.As(err, &pathErr) {
    fmt.Println("Problem with path:", pathErr.Path)
}

Quick Reference

Function Question It Answers
errors.Is(err, target) Is this error equal to target?
errors.As(err, &target) Can I treat this error as type target?

🚦 Sentinel Errors

What Are They?

Sentinel errors are like traffic lights - predefined signals everyone recognizes.

var ErrNotFound = errors.New("item not found")
var ErrPermission = errors.New("permission denied")
var ErrTimeout = errors.New("operation timed out")

Why Use Them?

Instead of checking error messages (which can change), you check the EXACT error:

// ❌ Bad - message might change
if err.Error() == "item not found" { ... }

// ✅ Good - checking the exact error
if errors.Is(err, ErrNotFound) { ... }

Naming Convention

Start sentinel errors with Err:

  • ErrNotFound
  • ErrInvalidInput
  • ErrTimeout

Real Example

package storage

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

func GetUser(id int) (*User, error) {
    user := findInDB(id)
    if user == nil {
        return nil, ErrNotFound
    }
    return user, nil
}
// In another package
user, err := storage.GetUser(123)
if errors.Is(err, storage.ErrNotFound) {
    fmt.Println("User doesn't exist!")
}

🎨 Custom Error Types

When Simple Errors Aren’t Enough

Sometimes you need errors that carry extra information. Like a doctor’s note that includes your temperature, not just “you’re sick.”

Creating a Custom Error Type

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

Using Custom Errors

func ValidateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "cannot be negative",
        }
    }
    return nil
}

func main() {
    err := ValidateAge(-5)

    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("Field '%s' has error: %s\n",
            valErr.Field, valErr.Message)
    }
}

Output: Field 'age' has error: cannot be negative

Custom Error with Extra Methods

type HTTPError struct {
    StatusCode int
    Message    string
}

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

func (e *HTTPError) IsClientError() bool {
    return e.StatusCode >= 400 &&
           e.StatusCode < 500
}

🗺️ The Complete Picture

graph TD A["Error Handling in Go"] --> B["error Interface"] A --> C["Creating Errors"] A --> D["Error Wrapping"] A --> E["errors.Join"] A --> F["Error Inspection"] A --> G["Sentinel Errors"] A --> H["Custom Error Types"] C --> C1["errors.New"] C --> C2["fmt.Errorf"] F --> F1["errors.Is"] F --> F2["errors.As"]

💡 Golden Rules

  1. Always check errors - Never ignore them!
  2. Wrap errors - Add context as errors bubble up
  3. Use sentinel errors - For errors you want to compare
  4. Use custom types - When you need extra data
  5. Use errors.Is() and errors.As() - Not string comparison

🎉 You Did It!

You now understand Go’s error handling system! Remember:

Errors are your friends, not your enemies. They tell you exactly what went wrong so you can fix it!

Like a good detective, you now have all the tools to investigate any problem in your Go programs. Happy coding! 🚀

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.