Go: Go Error Handling and Package Management

Last updated: 2026-08-26

Errors are values, not exceptions—Go treats errors as ordinary return values, and this design makes خطأ handling explicit, controllable, and composable.

Go's خطأ-handling philosophy and package management are the cornerstones of production-grade code. In this lesson, you'll master Go's two most underrated core capabilities.

1. You will learn



2. A True Story of a Microservices Engineer

(1) Pain Point: Online panic caused the service to crash, and 500 errors flooded the alert group

Alice is a واجهة خلفية engineer on the microservices team. The user service she maintains has recently encountered a major problem:

"The user service crashed three times last week, each time due to a nil pointer dereference. Whenever the Go service panics, the entire process shuts down, and no users can log in—the product manager said that if it crashes one more time, bonuses will be docked."

She opened the خطأ code at the scene of the malfunction:

GO
// Bad code: No error handling; it just panics.
func getUserByID(db *sql.DB, id int) *User {
    rows, _ := db.Query("SELECT * FROM users WHERE id = ?", id)
    // If the ID does not exist, rows.Next() returns false.
    // However, directly accessing the value below—the rows.Scan operation on nil—causes a panic.
    var user User
    for rows.Next() {
        rows.Scan(&user.Name, &user.Age)
    }
    return &user
}

Three issues: (1) Errors in db.Query are ignored; (2) The existence of the result is not checked; (3) A panic causes the entire process to crash.

(2) The Go solution: Errors are values

GO
// user_service.go
package main

import (
    "errors"
    "fmt"
)

// Custom Error Types
type NotFoundError struct {
    ID int
}

func (e NotFoundError) Error() سلسلة {
    return fmt.Sprintf("user %d not found", e.ID)
}

// Error Sentinel
var ErrInvalidInput = errors.New("invalid input")

// Robust Query Function
func findUser(id int) (*User, خطأ) {
    if id <= 0 {
        return nil, fmt.Errorf("findUser: %w", ErrInvalidInput)
    }

    users := map[int]User{
        1: {Name: "Alice", Age: 28},
        2: {Name: "Bob", Age: 32},
    }

    user, ok := users[id]
    if !ok {
        return nil, NotFoundError{ID: id}
    }
    return &user, nil
}

type User struct {
    Name سلسلة
    Age  int
}

func main() {
    for _, id := range []int{1, -1, 999} {
        user, err := findUser(id)
        if err != nil {
            // Determining the Type of Error
            if errors.Is(err, ErrInvalidInput) {
                fmt.Printf("Input خطأ (skipped): %v\n", err)
                continue
            }
            var nf NotFoundError
            if errors.As(err, &nf) {
                fmt.Printf("User %d does not exist\n", nf.ID)
                continue
            }
            fmt.Printf("Unknown خطأ: %v\n", err)
            continue
        }
        fmt.Printf("Found: %s (%d)\n", user.Name, user.Age)
    }
}

Output:

TEXT 📖 Display only
Found: Alice (28)
Input error (skipped): findUser: invalid input
User 999 does not exist

(3) Benefits: Comparison of Error Handling

Dimension try-catch language Go خطأ
Error is Exception Control Flow Normal Return Value
Explicit Implicit (easy to miss the catch block) Explicit if err != nil
Performance Stack-unwinding overhead No additional overhead
Composability Poor (abnormally interrupts the flow) Good (err can be freely passed)
💡 Tip: When exceptions are thrown in Java, C++, or Python, stack unwinding occurs; in Go, an خطأ is simply an interface value (16 bytes), so passing it involves virtually no overhead.



3. خطأ interface

(1) What is an خطأ?

GO
type error interface {
    Error() string
}

Any type that implements the Error() string method is an error.

▶ Example: 4 Ways to Create an Error

GO
package main

import (
    "errors"
    "fmt"
)

// Method 1: errors.New (most commonly used)
var ErrNotFound = errors.New("resource not found")

// Method 2: fmt.Errorf (with formatting)
func validate(age int) خطأ {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be >= 0)", age)
    }
    return nil
}

// Method 3: Wrap the خطأ in fmt.Errorf (%w)
func loadConfig(path سلسلة) خطأ {
    if path == "" {
        return fmt.Errorf("loadConfig: %w", ErrNotFound)
    }
    return nil
}

// Method 4: Customizing the خطأ type
type TimeoutError struct {
    DurationMs int
    Operation  سلسلة
}

func (e TimeoutError) Error() سلسلة {
    return fmt.Sprintf("%s timed out after %dms", e.Operation, e.DurationMs)
}

func main() {
    // Method 1
    fmt.Println(ErrNotFound)  // resource not found

    // Method 2
    fmt.Println(validate(-5))  // invalid age: -5 (must be >= 0)

    // Method 3
    fmt.Println(loadConfig(""))  // loadConfig: resource not found

    // Method 4
    err := TimeoutError{DurationMs: 5000, Operation: "DB query"}
    fmt.Println(err)  // DB query timed out after 5000ms
}
▶ Try it Yourself

(3) Comparison of the Four Creation Methods

Method Function/Syntax Purpose Supports error chaining?
errors.New errors.New("msg") Simple static error
fmt.Errorf fmt.Errorf("msg %d", n) Formatted error
fmt.Errorf(%w) fmt.Errorf("ctx: %w", err) Wrapped error ✅ errors.Is/As
Custom Type struct { ... Error() string } Error with additional fields ✅ Custom


4. errors.Is / errors.As error chain

(1) errors.Is: Checks whether a particular sentinel is included in the error chain

GO
package main

import (
    "errors"
    "fmt"
)

var ErrDB = errors.New("database error")
var ErrConn = fmt.Errorf("connection failed: %w", ErrDB)

func main() {
    err := fmt.Errorf("query failed: %w", ErrConn)

    // errors.Is searches layer by layer along the %w chain
    fmt.Println(errors.Is(err, ErrDB))    // true
    fmt.Println(errors.Is(err, ErrConn))  // true

    // == Can only match the outermost level
    fmt.Println(err == ErrDB)   // false (different objects)
    fmt.Println(err == ErrConn) // false
}

▶ Example: errors.As: Extracts specific types of errors from the chain

GO
package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field string
    Value interface{}
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s = %v", e.Field, e.Value)
}

func process(input string) error {
    if input == "" {
        return ValidationError{Field: "input", Value: ""}
    }
    return nil
}

func main() {
    err := process("")

    // errors.As: Extracts the ValidationError type from the chain
    var valErr ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("Field %s is invalid, value=%v\n", valErr.Field, valErr.Value)
    }

    // Also works with wrapping
    wrapped := fmt.Errorf("process failed: %w", err)
    var valErr2 ValidationError
    if errors.As(wrapped, &valErr2) {
        fmt.Printf("(After wrapping) Field %s is invalid\n", valErr2.Field)
    }
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
Field input is invalid, value=
(After wrapping) Field input is invalid

(3) errors.Is vs errors.As

Function Matching Method Purpose
errors.Is(err, target) Equal to (==) Checks whether a specific sentinel error has occurred
errors.As(err, &target) Type matching Retrieve an error of a specific type from the error chain


5. panic / recover

(1) panic: unrecoverable error

GO
package main

import "fmt"

func main() {
    fmt.Println("Start")

    // A panic immediately terminates the current دالة and begins stack unwinding.
    panic("something went terribly wrong")

    // This line will not be executed
    fmt.Println("End")
}

Output:

TEXT 📖 Display only
Start
panic: something went terribly wrong

goroutine 1 [running]:
main.main()
        /tmp/main.go:8 +0x...
exit status 2

▶ Example: recover (to recover from a panic)

GO
package main

import (
    "fmt"
)

// recover is only useful in defer
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()

    // Intentionally triggering a panic
    if b == 0 {
        panic("division by zero")
    }
    return a / b, nil
}

func main() {
    // Normal call
    if r, err := safeDivide(10, 2); err == nil {
        fmt.Printf("10/2 = %d\n", r)
    }

    // A panic is caught by recover and does not cause a crash
    if r, err := safeDivide(10, 0); err != nil {
        fmt.Printf("Error: %v (result=%d)\n", err, r)
    }

    fmt.Println("Program ended normally—panic was recovered")
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
10/2 = 5
Error: panic recovered: division by zero (result=0)
Program ended normally—panic was recovered

(3) Use Cases for panic vs. error

Scenario Use error Use panic
User input error
File does not exist
Network Timeout
nil pointer dereference ❌ (cannot be recovered) ✅ (code bug)
Array index out of bounds ❌ (Not checked by the compiler) ✅ (Code bug)
Initialization failed (required condition)
🔥 Common Mistake: panic + recover should not be used to simulate a try-catch block. Go's philosophy is "use panic sparingly, and use error more often." panic should only be used for true exceptional conditions (code bugs, initialization failures, or unrecoverable states).



6. Go Mod Package Management

(1) The Three Main Commands in Go Mod

Command Function Common Use Cases
go mod init <module> Initialize a module Start a new project
go mod tidy Clean up dependencies (add missing ones, remove unnecessary ones) After modifying the import statements
go mod add <path>@<ver> Add a dependency (New in Go 1.22+) To add an external package
go get <path>@<ver> Add/Update Dependencies Traditional Method

▶ Example: Create a module + Add dependencies

BASH
# 1. Initialize module
$ go mod init github.com/alice/user-service
go: creating new go.mod: module github.com/alice/user-service

# 2. Import an external package in the code
GO
package main

import (
    "fmt"
    "github.com/google/uuid"  // external dependency
)

func main() {
    id := uuid.New()
    fmt.Printf("Generated UUID: %s\n", id)
}
BASH
# 3. Add dependencies and organize
$ go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.6.0

# 4. View the generated go.mod
$ cat go.mod
module github.com/alice/user-service

go 1.22

require github.com/google/uuid v1.6.0

(3) Package Export Rules

GO
// math/calculator.go
package math

// Uppercase = Public (accessible to other packages)
func Add(a, b int) int { return a + b }
var Version = "1.0"

// Lowercase first letter = private (visible only within the package)
func helper(x int) int { return x * 2 }
var internalVersion = "0.5"

// Public Structure
type Calculator struct {
    // Public field
    Name string
    // Private field (cannot be accessed directly from outside the package)
    precision int
}
GO
package main

import "yourmodule/math"

func main() {
    math.Add(1, 2)      // ✅ Public
    math.Version        // ✅ Public متغير

    // math.helper(5)   // ❌ Private دالة; compilation خطأ
    // math.internalVersion  // ❌ Private متغير

    c := math.Calculator{Name: "basic"}  // ✅ Public struct
    // c.precision = 2  // ❌ Private field; compilation خطأ
}

▶ Example: Package Export + Error Type Passing

GO
// apperrors/errors.go
package apperrors

import "fmt"

// Public Error Type (Uppercase)
type BusinessError struct {
    Code    int
    Message string
}

func (e BusinessError) Error() string {
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// Public Sentinel
var ErrUnauthorized = BusinessError{Code: 401, Message: "unauthorized"}

// Private error (external packages cannot reference directly)
type internalError struct {
    detail string
}

func (e internalError) Error() string {
    return fmt.Sprintf("internal: %s", e.detail)
}

// Public factory function (external packages use internalError indirectly through this function)
func NewInternalError(detail string) error {
    return internalError{detail: detail}
}
▶ Try it Yourself

7. Complete Example: A Robust User Service

Linking خطأ handling, package management, and custom errors together:

GO
// user_service.go
package main

import (
    "errors"
    "fmt"
)

// ---------- Error Definitions ----------

type NotFoundError struct {
    Resource string
    ID       int
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
}

type ValidationError struct {
    Field   string
    Message string
}

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

type DBError struct {
    Operation string
    Err       error
}

func (e DBError) Error() string {
    return fmt.Sprintf("db %s failed: %v", e.Operation, e.Err)
}

func (e DBError) Unwrap() error {
    return e.Err
}

// Sentinel Error
var ErrInternal = errors.New("internal server error")

// ---------- Data Layer (Simulated DB) ----------

type User struct {
    ID   int
    Name string
    Age  int
}

func queryUserFromDB(id int) (*User, error) {
    db := map[int]User{
        1: {ID: 1, Name: "Alice", Age: 28},
        2: {ID: 2, Name: "Bob", Age: 32},
    }
    user, ok := db[id]
    if !ok {
        return nil, NotFoundError{Resource: "user", ID: id}
    }
    return &user, nil
}

// ---------- Service Layer ----------

func GetUser(id int) (*User, error) {
    // panic protection
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("[PANIC] recovered: %v\n", r)
        }
    }()

    if id <= 0 {
        return nil, ValidationError{
            Field:   "id",
            Message: "must be positive",
        }
    }

    user, err := queryUserFromDB(id)
    if err != nil {
        var nf NotFoundError
        if errors.As(err, &nf) {
            return nil, nf
        }
        return nil, DBError{
            Operation: "queryUserFromDB",
            Err:       err,
        }
    }

    if user.Age < 0 || user.Age > 150 {
        return nil, ValidationError{
            Field:   "age",
            Message: fmt.Sprintf("unexpected age: %d", user.Age),
        }
    }

    return user, nil
}

// ---------- HTTP Layer ----------

func HandleGetUser(id int) {
    user, err := GetUser(id)
    if err != nil {
        var nf NotFoundError
        var ve ValidationError
        var de DBError

        switch {
        case errors.As(err, &nf):
            fmt.Printf("[404] %v\n", err)
        case errors.As(err, &ve):
            fmt.Printf("[400] %v\n", err)
        case errors.As(err, &de):
            fmt.Printf("[500] db error: %v\n", de)
            fmt.Printf("[500] Internal: %+v\n", de.Err)
        default:
            fmt.Printf("[500] %v\n", err)
        }
        return
    }
    fmt.Printf("[200] User: %+v\n", user)
}

func main() {
    // Normal
    HandleGetUser(1)

    // Input error (ValidationError with additional information)
    HandleGetUser(0)

    // User does not exist (custom NotFoundError)
    HandleGetUser(999)

    fmt.Println("\n=== Program Exited Normally ===")
}

Expected Output:

TEXT 📖 Display only
[200] User: &{ID:1 Name:Alice Age:28}
[400] validation failed: id - must be positive
[404] user with id 999 not found

=== Program Exited Normally ===
100%
flowchart TD
    A[Function returns error] --> B{err == nil?}
    B -->|Yes| C[Normal processing]
    B -->|No| D[Determine error type]
    D --> E[errors.Is / == sentinel]
    D --> F[errors.As / type assertion]
    D --> G[type switch]
    E --> H[Handle specific sentinel error]
    F --> I[Extract structured error info]
    G --> J[Branch by type]
    H --> K[Return or retry]
    I --> K
    J --> K
🔥 Common Mistake: The Unwrap() error method on the DBError struct is key to allowing custom errors to participate in the error chain. If a custom type does not have an Unwrap() method, errors.Is and errors.As will only check the outermost layer.


❓ FAQ

Q What type is error?
A error is a built-in interface: type error interface { Error() string }. Any type that implements the Error() string method is an error—a 16-byte interface value.
Q How do I customize errors?
A Define a struct and implement the Error() string method. If you want to support error chaining (errors.Is/As traversal), add an Unwrap() error method that returns the inner error.
Q Is it necessary to use recover after panic?
A Not necessarily. recover is only useful within a defer block, and should only be placed at the entrance to a goroutine (go func() { defer recover() }). Do not use recover in your business logic—that masks bugs rather than fixing them.
Q What is the difference between errors.Is and errors.As?
A errors.Is(err, target) performs value comparisons along the %w chain, level by level (==); errors.As(err, &target) performs type assertions step by step along the chain and populates target. Simply put: Is checks the value, while As extracts the type.
Q How does go mod manage dependencies?
A Core workflow: go mod init to initialize → write code and use importgo mod tidy to automatically download and organize → lock versions using go.mod and go.sum. Go 1.22+ introduces the go mod add command, which is more intuitive.
Q What are the rules for public and private names?
A There is one rule: names starting with an uppercase letter are public (exported), while those starting with a lowercase letter are private. This applies to variables, functions, types, struct fields, and constants. There are no public/private keywords.
Q What is the difference between fmt.Errorf(%w) and fmt.Errorf(%v)?
A %w creates an error with an error chain that can be traversed by errors.Is/As; %v simply formats a string and creates a new error that is unrelated to the original error.
Q How can errors be handled gracefully in production code?
A (1) Nest fmt.Errorf("context: %w", err) to preserve the error chain; (2) Define business error types with additional fields; (3) Uniformly resolve errors at the HTTP handler layer → HTTP status codes; (4) Log the complete chain (%+v).

📖 Summary


📝 Exercises

  1. Basic Problem (Difficulty ⭐): Define a Divide function func Divide(a, b float64) (float64, error) that returns errors.New("division by zero") if the divisor is 0, and otherwise returns the quotient.

  2. Advanced Problem (Difficulty ⭐⭐): Implement a ConfigLoader that supports loading configuration from a JSON file and falling back to environment variables. Requirements: Wrap each error level with fmt.Errorf(%w), and allow the caller to use errors.Is to determine whether the error is "file not found" or "JSON parsing error."

  3. Challenge Problem (Difficulty ⭐⭐⭐): Build a three-tier error-handling architecture: (1) Data layer Repository → Returns NotFoundError / DBError; (2) Service layer Service → Wraps data layer errors + adds ValidationError; (3) HTTP handler → Parse errors layer by layer using errors.As and map them to HTTP status codes (404/400/500). The error structure must include business fields (ID/Field/Operation).

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏