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
- The
خطأinterface and 4 ways to create it errors.Is/errors.Asto check the خطأ chain- Custom خطأ types
- Use Cases for
panic/recover go moddependency management (init/tidy/add)- Package export rules (uppercase = public, lowercase = private)
- Building User Services with Robust Error Handling
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:
// 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
// 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:
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) |
خطأ is simply an interface value (16 bytes), so passing it involves virtually no overhead.
3. خطأ interface
(1) What is an خطأ?
type error interface {
Error() string
}
Any type that implements the Error() string method is an error.
▶ Example: 4 Ways to Create an Error
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
}
(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
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
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)
}
}
Output:
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
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:
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)
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")
}
Output:
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) | ❌ | ✅ |
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
# 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
package main
import (
"fmt"
"github.com/google/uuid" // external dependency
)
func main() {
id := uuid.New()
fmt.Printf("Generated UUID: %s\n", id)
}
# 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
// 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
}
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
// 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}
}
7. Complete Example: A Robust User Service
Linking خطأ handling, package management, and custom errors together:
// 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:
[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 ===
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
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
error?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.Error() string method. If you want to support error chaining (errors.Is/As traversal), add an Unwrap() error method that returns the inner error.recover after panic?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.errors.Is and errors.As?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.go mod manage dependencies?go mod init to initialize → write code and use import → go 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.public/private keywords.fmt.Errorf(%w) and fmt.Errorf(%v)?%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.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
erroris a built-in interface; any type that implementsError() stringis anerror- 4 ways to create errors:
errors.New/fmt.Errorf/%wwrapping / custom types errors.Ischecks whether the target is included in the error chain (value match)errors.Asextracts errors of a specified type from the error chain (based on type matching)panicis used for unrecoverable errors;recoveris only valid within adeferblockgo mod init/tidy/addto manage external dependencies- There is only one rule for package export: uppercase = public, lowercase = private
- Production Code: Nested Errors + Custom Error Types + Unified Handling
📝 Exercises
-
Basic Problem (Difficulty ⭐): Define a
Dividefunctionfunc Divide(a, b float64) (float64, error)that returnserrors.New("division by zero")if the divisor is 0, and otherwise returns the quotient. -
Advanced Problem (Difficulty ⭐⭐): Implement a
ConfigLoaderthat supports loading configuration from a JSON file and falling back to environment variables. Requirements: Wrap each error level withfmt.Errorf(%w), and allow the caller to useerrors.Isto determine whether the error is "file not found" or "JSON parsing error." -
Challenge Problem (Difficulty ⭐⭐⭐): Build a three-tier error-handling architecture: (1) Data layer
Repository→ ReturnsNotFoundError/DBError; (2) Service layerService→ Wraps data layer errors + addsValidationError; (3) HTTP handler → Parse errors layer by layer usingerrors.Asand map them to HTTP status codes (404/400/500). The error structure must include business fields (ID/Field/Operation).