A logo showing the text blog.marcnuri.com
Español
Home»Backend Development»Error Handling Best Practices in Go

Recent Posts

  • Fabric8 Kubernetes Client 7.4 is now available!
  • Kubernetes MCP Server Joins the Containers Organization!
  • MCP Tool Annotations: Adding Metadata and Context to Your AI Tools
  • Fabric8 Kubernetes Client 7.2 is now available!
  • Connecting to an MCP Server from JavaScript using AI SDK

Categories

  • Artificial Intelligence
  • Backend Development
  • Cloud Native
  • Engineering Insights
  • Frontend Development
  • JavaScript
  • Legacy
  • Operations
  • Personal
  • Pet projects
  • Quality Engineering
  • Tools

Archives

  • September 2025
  • July 2025
  • May 2025
  • April 2025
  • March 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • August 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • July 2022
  • June 2022
  • May 2022
  • March 2022
  • February 2022
  • January 2022
  • December 2021
  • November 2021
  • October 2021
  • September 2021
  • August 2021
  • July 2021
  • January 2021
  • December 2020
  • November 2020
  • October 2020
  • September 2020
  • August 2020
  • July 2020
  • June 2020
  • May 2020
  • March 2020
  • February 2020
  • January 2020
  • December 2019
  • October 2019
  • September 2019
  • July 2019
  • March 2019
  • November 2018
  • July 2018
  • June 2018
  • May 2018
  • April 2018
  • March 2018
  • February 2018
  • December 2017
  • October 2017
  • August 2017
  • July 2017
  • January 2017
  • December 2015
  • November 2015
  • December 2014
  • March 2014
  • February 2011
  • November 2008
  • June 2008
  • May 2008
  • April 2008
  • January 2008
  • November 2007
  • September 2007
  • August 2007
  • July 2007
  • June 2007
  • May 2007
  • April 2007
  • March 2007

Error Handling Best Practices in Go

2020-03-07 in Backend Development tagged Go / Best Practices / Debugging by Marc Nuri | Last updated: 2025-09-21
Versión en Español

Introduction

Error handling is one of the most distinctive aspects of Go programming. Unlike languages that use exceptions such as Java, Go treats errors as values that must be explicitly handled. At first, this approach may feel repetitive, but in practice it leads to code that is easier to reason about and debug.

In this post, I'll go beyond the basics and share Go's error handling philosophy, common patterns, and the traps I've seen developers (myself included) fall into. We'll look at idiomatic practices, custom error types, wrapping, debugging strategies, and how to apply them in real applications (web, database, concurrency). By the end, you should be able to design error flows that are both reliable for users and maintainable for developers.

Go's Error Handling Philosophy

Go's approach to error handling is based on several key principles:

  1. Errors are values: they behave like regular return values.
  2. Explicit handling: you must check and act on them.
  3. No hidden control flow: no try/catch blocks changing execution unexpectedly.
  4. Fail fast: surface problems early instead of letting them propagate silently.

This explicit style forces developers to think about error cases right where they occur.

The error interface

In Go, an error is any type that implements the error interface:

error_interface.go
type error interface {
    Error() string
}

A basic error can be created with errors.New():

basic_errors.go
package main

import (
    "errors"
    "fmt"
)

func divide(n, d float64) (float64, error) {
    if d == 0 {
        return 0, errors.New("division by zero")
    }
    return n / d, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("Result: %.2f\n", result)
}

In this example, divide returns an error if the denominator is zero.

In the main function, we follow the key principles:

  • We check the error immediately after the function call.
  • We handle it explicitly by printing a message and returning early.
  • If there's no error, we proceed to use the result.

Basic Error Handling Patterns

The standard pattern

The most common way to handle errors is:

standard_pattern.go
result, err := aFunction()
if err != nil {
    // Handle the error and return early
    return err
}
// Continue with the result

Antipattern: ignoring the error:

// ❌ Don't do this
result, _ := aFunction()

This often hides real problems until production.

Multiple return values

Go functions often return both a value and an error:

multiple_returns.go
package main

import (
    "fmt"
    "strconv"
)

func parseAge(s string) (int, error) {
    age, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("invalid age format: %w", err)
    }

    if age < 0 || age > 150 {
        return 0, fmt.Errorf("age %d is out of valid range (0-150)", age)
    }

    return age, nil
}

func main() {
    validCases := []string{"25", "0", "150"}
    invalidCases := []string{"abc", "-5", "200", "25.5"}

    fmt.Println("Valid cases:")
    for _, ageStr := range validCases {
        age, err := parseAge(ageStr)
        if err != nil {
            fmt.Printf("❌ %s: %v\n", ageStr, err)
        } else {
            fmt.Printf("✅ %s: %d\n", ageStr, age)
        }
    }

    fmt.Println("\nInvalid cases:")
    for _, ageStr := range invalidCases {
        age, err := parseAge(ageStr)
        if err != nil {
            fmt.Printf("❌ %s: %v\n", ageStr, err)
        } else {
            fmt.Printf("✅ %s: %d\n", ageStr, age)
        }
    }
}

In this example, parseAge returns both the parsed age and an error if the input is invalid. The main function demonstrates handling both valid and invalid inputs, printing appropriate messages for each case.

Early returns

Avoid deeply nested if blocks by returning early:

early_returns.go
func processUser(userID string) error {
    user, err := getUserByID(userID)
    if err != nil {
        return fmt.Errorf("failed to get user: %w", err)
    }

    if !user.IsActive {
        return fmt.Errorf("user %s is not active", userID)
    }

    err = validateUser(user)
    if err != nil {
        return fmt.Errorf("user validation failed: %w", err)
    }

    err = saveUser(user)
    if err != nil {
        return fmt.Errorf("failed to save user: %w", err)
    }

    return nil
}

In this example, each error is handled immediately, keeping the code flat and readable.

Custom Error Types

Custom errors add context and can be matched more precisely. They're useful when your domain has distinct error classes (validation, network, database).

custom_errors.go
package main

import (
    "fmt"
    "net/http"
    "strings"

)

// ValidationError represents a validation error
type ValidationError struct {
    Field   string
    Message string
}

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

// HttpError represents an http-related error
type HttpError struct {
    StatusCode int
    URL        string
    Err        error
}

func (e *HttpError) Error() string {
    return fmt.Sprintf("http error (%d) for URL %s: %v", e.StatusCode, e.URL, e.Err)
}

func (e *HttpError) Unwrap() error {
    return e.Err
}

// DatabaseError represents a database error
type DatabaseError struct {
    Operation string
    Table     string
    Err       error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error during %s on table %s: %v", e.Operation, e.Table, e.Err)
}

func (e *DatabaseError) Unwrap() error {
    return e.Err
}

// Example functions that return custom errors
func validateEmail(email string) error {
    if email == "" {
        return &ValidationError{
            Field:   "email",
            Message: "email is required",
        }
    }

    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Message: "email must contain @ symbol",
        }
    }

    return nil
}

func fetchUserData(url string) error {
    // Simulate http request
    if url == "" {
        return &HttpError{
            StatusCode: http.StatusBadRequest,
            URL:        url,
            Err:        fmt.Errorf("empty URL provided"),
        }
    }

    // Simulate http error
    return &HttpError{
        StatusCode: http.StatusNotFound,
        URL:        url,
        Err:        fmt.Errorf("user not found"),
    }
}

func saveUserToDB(userID string) error {
    if userID == "" {
        return &DatabaseError{
            Operation: "INSERT",
            Table:     "users",
            Err:       fmt.Errorf("user ID cannot be empty"),
        }
    }

    return nil
}

func main() {
    // Test validation error
    err := validateEmail("invalid-email")
    if err != nil {
        fmt.Printf("Validation error: %v\n", err)
    }

    // Test network error
    err = fetchUserData("")
    if err != nil {
        fmt.Printf("Network error: %v\n", err)
    }

    // Test database error
    err = saveUserToDB("")
    if err != nil {
        fmt.Printf("Database error: %v\n", err)
    }
}

In this example, we define three custom error types: ValidationError, HttpError, and DatabaseError. Each type includes relevant fields and implements the Error() method to provide a descriptive message.

The main function calls functions that return these custom errors and prints the error messages by using the %v verb in fmt.Printf, which invokes the Error() method of each error type.

Tip

Don't create custom error types for every case. Use them only where downstream code really needs to distinguish one failure from another.

Error Wrapping and Unwrapping

Go 1.13 introduced wrapping, allowing you to add context without losing the original cause.

Basic error wrapping

error_wrapping.go
package main

import (
    "errors"
    "fmt"
    "os"
)

func readConfig(filename string) error {
    _, err := os.Open(filename)
    if err != nil {
        return fmt.Errorf("failed to read config file %s: %w", filename, err)
    }
    return nil
}

func initialize() error {
    err := readConfig("blog.marcnuri.com.config.yaml")
    if err != nil {
        return fmt.Errorf("initialization failed: %w", err)
    }
    return nil
}

func main() {
    err := initialize()
    if err != nil {
        fmt.Printf("Application error: %v\n", err)

        // Check for specific error types
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("The config file doesn't exist")
        }

        // Unwrap to get the original error
        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            fmt.Printf("Path error occurred: %s\n", pathErr.Path)
        }
    }
}

In this example, readConfig wraps the error returned by os.Open with additional context. The initialize function further wraps this error. In main, we print the full error chain and use errors.Is and errors.As to check for specific error types.

Let's see how to use these functions effectively.

The errors.Is and errors.As functions

These functions help you detect or extract wrapped errors:

errors_is_as.go
package main

import (
    "errors"
    "fmt"
    "os"
)

var (
    ErrUserNotFound = errors.New("user not found")
    ErrInvalidInput = errors.New("invalid input")
)

func findUser(id string) error {
    if id == "" {
        return fmt.Errorf("empty user ID: %w", ErrInvalidInput)
    }

    if id == "404" {
        return fmt.Errorf("user lookup failed: %w", ErrUserNotFound)
    }

    return nil
}

func main() {
    testCases := []string{"", "404", "123"}

    for _, userID := range testCases {
        err := findUser(userID)
        if err != nil {
            fmt.Printf("User ID %s: %v\n", userID, err)

            // Check for specific errors using errors.Is
            if errors.Is(err, ErrUserNotFound) {
                fmt.Println("  → This is a user not found error")
            }

            if errors.Is(err, ErrInvalidInput) {
                fmt.Println("  → This is an invalid input error")
            }

            // Check for system errors
            var pathErr *os.PathError
            if errors.As(err, &pathErr) {
                fmt.Printf("  → Path error: %s\n", pathErr.Path)
            }
        } else {
            fmt.Printf("User ID %s: Success\n", userID)
        }
        fmt.Println()
    }
}

In this example, we define two sentinel errors: ErrUserNotFound and ErrInvalidInput. These errors are wrapped with context in the findUser function depending on the input.

In the main function, we test various user IDs and print the resulting errors. We use errors.Is to check if the error matches our sentinel errors and errors.As to check for system errors like os.PathError.

Tip

Avoid wrapping at every single layer. Wrap only when you're adding useful context; otherwise you risk noisy, unreadable error chains.

Error Handling in Different Contexts

Now that we've covered the fundamentals, let's see how to apply these patterns in real-world scenarios.

Web applications

When building APIs, a common pattern is to convert Go errors into structured JSON responses that clients can understand.

Here's a concise example that shows how to define a custom error type and a helper function to map errors into HTTP status codes:

web_errors.go
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
)

type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}

func (e *APIError) Error() string { return e.Message }

func writeError(w http.ResponseWriter, err error) {
    var apiErr *APIError
    if !errors.As(err, &apiErr) {
        apiErr = &APIError{"INTERNAL_ERROR", "internal server error"}
    }

    status := map[string]int{
        "USER_NOT_FOUND": http.StatusNotFound,
        "INVALID_INPUT":  http.StatusBadRequest,
    }[apiErr.Code]
    if status == 0 {
        status = http.StatusInternalServerError
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(apiErr)
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        writeError(w, &APIError{"INVALID_INPUT", "user ID is required"})
        return
    }
    if id == "404" {
        writeError(w, &APIError{"USER_NOT_FOUND", fmt.Sprintf("no user with id %s", id)})
        return
    }
    json.NewEncoder(w).Encode(map[string]string{"id": id, "name": "John Doe"})
}

func main() {
    http.HandleFunc("/user", getUserHandler)
    http.ListenAndServe(":8080", nil)
}

In this example, we use a single APIError type to represent application errors and a writeError helper to translate them into JSON responses with the right HTTP status code. The helper uses errors.As to check whether the incoming error is already an APIError; otherwise, it defaults to a generic internal error.

This pattern keeps handlers short and focused, while ensuring clients consistently receive clear, structured responses.

Database operations

Database operations often return driver-specific errors such as sql.ErrNoRows that need careful handling. Here's a simplified example showing how to wrap, propagate, and inspect errors using Go's errors.Is and sentinel errors:

database_errors.go
package main

import (
    "database/sql"
    "errors"
    "fmt"
    "log"
)

// User represents a user in the system
type User struct {
    ID    int
    Name  string
    Email string
}

// Sentinel errors
var (
    ErrUserNotFound  = errors.New("user not found")
    ErrDuplicateUser = errors.New("duplicate user")
)

// UserRepository handles user database operations
type UserRepository struct {
    db *sql.DB
}

func (r *UserRepository) GetUser(id int) (*User, error) {
    user := &User{}
    err := r.db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).
        Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, fmt.Errorf("user %d not found: %w", id, ErrUserNotFound)
        }
        return nil, fmt.Errorf("failed to query user: %w", err)
    }
    return user, nil
}

func (r *UserRepository) CreateUser(user *User) error {
    _, err := r.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", user.Name, user.Email)
    if err != nil {
        if isDuplicateKeyError(err) {
            return fmt.Errorf("user with email %s already exists: %w", user.Email, ErrDuplicateUser)
        }
        return fmt.Errorf("failed to create user: %w", err)
    }
    return nil
}

// Stub function for duplicate check
func isDuplicateKeyError(err error) bool { return false }

func main() {
    var db *sql.DB // pretend this is initialized
    repo := &UserRepository{db: db}

    user, err := repo.GetUser(1)
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            log.Printf("User not found: %v", err)
        } else {
            log.Printf("Database error: %v", err)
        }
        return
    }

    fmt.Printf("Found user: %+v\n", user)
}

This example demonstrates how to handle database errors concisely. The GetUser function returns wrapped errors with context and uses errors.Is to check for the sentinel sql.ErrNoRows. The CreateUserfunction, similarly, wraps errors and shows how you could handle specific database constraints, like duplicate keys.

The main function illustrates how to call the repository methods and handle errors appropriately by checking for the domain-specific sentinel errors we defined.

By keeping the code flat and focused, you can see idiomatic Go practices for propagating and inspecting errors without unnecessary boilerplate.

Goroutines and Error Handling

Concurrent code needs explicit error collection. A common pattern is to send results (including errors) through channels:

goroutine_errors.go
package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

// Result represents the result of an operation
type Result struct {
    Value int
    Error error
}

// worker performs work and sends results through a channel
func worker(ctx context.Context, id int, jobs <-chan int, results chan<- Result) {
    for {
        select {
        case job, ok := <-jobs:
            if !ok {
                return // Jobs channel closed
            }

            // Simulate work that might fail
            if job%3 == 0 {
                results <- Result{
                    Value: 0,
                    Error: fmt.Errorf("worker %d: job %d failed (divisible by 3)", id, job),
                }
            } else {
                // Simulate some processing time
                time.Sleep(100 * time.Millisecond)
                results <- Result{
                    Value: job * job,
                    Error: nil,
                }
            }

        case <-ctx.Done():
            fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err())
            return
        }
    }
}

// main demonstrates error handling with goroutines
func main() {
    const numWorkers = 3
    const numJobs = 10

    jobs := make(chan int, numJobs)
    results := make(chan Result, numJobs)

    // Create context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Start workers
    var wg sync.WaitGroup
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            worker(ctx, workerID, jobs, results)
        }(w)
    }

    // Send jobs
    go func() {
        defer close(jobs)
        for j := 1; j <= numJobs; j++ {
            select {
            case jobs <- j:
            case <-ctx.Done():
                fmt.Println("Job sending cancelled")
                return
            }
        }
    }()

    // Collect results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Process results and handle errors
    var successCount, errorCount int
    for result := range results {
        if result.Error != nil {
            fmt.Printf("❌ Error: %v\n", result.Error)
            errorCount++
        } else {
            fmt.Printf("✅ Result: %d\n", result.Value)
            successCount++
        }
    }

    fmt.Printf("\nSummary: %d successful, %d failed\n", successCount, errorCount)
}

In this example, we define a Result struct to encapsulate both the value and any error from a worker. The worker function processes jobs and sends results through a channel. The main function sets up the job and result channels, starts multiple workers, and collects results while handling errors appropriately.

For a broader intro to concurrency, check out Go Concurrency Patterns: Goroutines and Channels.

Error Handling Best Practices

Now that we've covered the fundamentals and seen practical examples, let's summarize some best practices for error handling in Go.

1. Always check errors

Never ignore errors unless you have a specific reason:

check_errors.go
// ❌ Bad
result, _ := aFunction()

// ✅ Good
result, err := aFunction()
if err != nil {
    return fmt.Errorf("operation failed: %w", err)
}

2. Provide context in error messages

Error messages should be descriptive and provide context about what operation failed. They should also provide actionable information so that the caller can understand and potentially fix the issue.

contextual_errors.go
// ❌ Bad
return errors.New("failed")

// ✅ Good
return fmt.Errorf("failed to process blog.marcnuri.com user %s: invalid email format", userID)

Note

Error message should be lowercase since they are often wrapped into larger sentences.

3. Use error wrapping for call chains

Preserve the original error while adding context:

wrapping_errors.go
func processUser(userID string) error {
    user, err := getUser(userID)
    if err != nil {
        return fmt.Errorf("failed to process blog.marcnuri.com user %s: %w", userID, err)
    }
    // ... rest of the function
    return nil
}

Note the use of %w in fmt.Errorf to wrap the original error.

4. Create custom error types for domain-specific errors

Use custom domain error types when you need to convey specific error conditions that callers might want to check for:

custom_error_type.go
type UserNotFoundError struct {
    UserID string
}

func (e *UserNotFoundError) Error() string {
    return fmt.Sprintf("blog.marcnuri.com user %s not found", e.UserID)
}

5. Don't panic in libraries

Libraries should return errors, not panic. Reserve panic for truly exceptional situations:

no_panic_in_libraries.go
// ❌ Bad (in a library)
func divide(n, d int) int {
    if d == 0 {
        panic("division by zero")
    }
    return n / d
}

// ✅ Good
func divide(n, d int) (int, error) {
    if d == 0 {
        return 0, errors.New("division by zero")
    }
    return n / d, nil
}

6. Use errors.Is and errors.As for error checking

Use these functions to check for specific error values or types in wrapped errors and act accordingly:

errors_is_as_usage.go
// Check for specific error values
if errors.Is(err, ErrUserNotFound) {
    // Handle user not found
}

// Extract specific error types
var validationErr *ValidationError
if errors.As(err, &validationErr) {
    // Handle validation error
}

Debugging and Logging Errors

Effective logging is crucial for diagnosing issues in production. Let's see how to log errors effectively to improve the debugging experience.

Structured logging

Use structured logging to make error debugging easier:

structured_logging.go
package main

import (
    "errors"
    "log/slog"
    "os"
)

func processUser(userID string) error {
    return errors.New("database connection failed")
}

func main() {
    // Create structured logger
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    // Example error scenario
    userID := "Aitana"
    err := processUser(userID)
    if err != nil {
        logger.Error("Failed to process blog.marcnuri.com user",
            "user_id", userID,
            "error", err.Error(),
            "operation", "user_processing",
        )
    }
}

In this example, we use Go's slog package to create a structured logger that outputs JSON logs. When an error occurs in processUser, we log the error along with relevant context such as the user ID and operation name.

This structured approach makes it easier to filter and analyze logs, especially in production environments.

The output (formatted) will look like this:

{
  "time":"2015-10-21T04:29:00Z",
  "level":"ERROR",
  "msg":"Failed to process blog.marcnuri.com user",
  "user_id":"Aitana",
  "error":"database connection failed",
  "operation":"user_processing"
}

Error stack traces

In general, you can rely on wrapping and context to trace errors. However, in complex scenarios, capturing stack traces can help finding the root cause of errors.

Stack-tracing is not directly supported in Go's standard library. In the example below, we implement a simple custom error type that captures the stack trace when the error is created.

stack_trace.go
package main

import (
    "fmt"
    "runtime"
)

// ErrorWithStack wraps an error with stack trace information
type ErrorWithStack struct {
    Err   error
    Stack []uintptr
}

func (e *ErrorWithStack) Error() string {
    return e.Err.Error()
}

func (e *ErrorWithStack) Unwrap() error {
    return e.Err
}

// StackTrace returns the stack trace as strings
func (e *ErrorWithStack) StackTrace() []string {
    frames := runtime.CallersFrames(e.Stack)
    var trace []string

    for {
        frame, more := frames.Next()
        trace = append(trace, fmt.Sprintf("%s:%d %s", frame.File, frame.Line, frame.Function))
        if !more {
            break
        }
    }

    return trace
}

// NewErrorWithStack creates an error with stack trace
func NewErrorWithStack(err error) *ErrorWithStack {
    const depth = 32
    var pcs [depth]uintptr
    n := runtime.Callers(2, pcs[:])

    return &ErrorWithStack{
        Err:   err,
        Stack: pcs[0:n],
    }
}

func functionA() error {
    return functionB()
}

func functionB() error {
    return functionC()
}

func functionC() error {
    return NewErrorWithStack(errors.New("something went wrong"))
}

func main() {
    err := functionA()
    if err != nil {
        fmt.Printf("Error: %v\n", err)

        var stackErr *ErrorWithStack
        if errors.As(err, &stackErr) {
            fmt.Println("\nStack trace:")
            for _, frame := range stackErr.StackTrace() {
                fmt.Printf("  %s\n", frame)
            }
        }
    }
}

In this example, we define an ErrorWithStack type that captures the stack trace when the error is created by using the runtime.Callers function.

In the main function, we simulate a series of function calls that eventually return an error wrapped with a stack trace. When we detect that the error is of type ErrorWithStack, we print the stack trace to help diagnose where the error originated.

Caution

Stack traces can be expensive to capture, so reserve them for debugging builds or critical failures.

The output will look like this:

stack_trace_output.txt
Error: something went wrong
Stack trace:
  /home/blog.marcnuri.com/stack_trace.go:34 main.functionC
  /home/blog.marcnuri.com/stack_trace.go:29 main.functionB
  /home/blog.marcnuri.com/stack_trace.go:24 main.functionA
  /home/blog.marcnuri.com/stack_trace.go:40 main.main
  /usr/local/go/src/runtime/proc.go:225 runtime.main
  /usr/local/go/src/runtime/asm_amd64.s:1371 runtime.goexit

Conclusion

In this post, I've shown you how to handle errors explicitly, create custom error types, wrap and unwrap errors, and apply these patterns in various contexts such as web applications, database operations, and concurrent programming.

Effective error handling is crucial for building robust Go applications. By following these guidelines and Go's error handling philosophy and best practices, you'll be able to write Go code that gracefully handles errors, making your applications more resilient and easier to debug.

Remember, error handling is a design decision, it shapes the stability of your application and the experience of the next developer who maintains your code.

You might also like

  • How to Initialize a New Go Project with Modules
  • Go Concurrency Patterns: Goroutines and Channels
  • How to set up and tear down unit tests in Go
Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Post navigation
Kubernetes Client for Java: Introducing YAKCGo Concurrency Patterns: Goroutines and Channels
© 2007 - 2025 Marc Nuri