Go Error Handling Best Practices: From Basics to Advanced

The article explains why proper error handling is crucial for reliable Go programs, describes Go's explicit error return model, demonstrates basic techniques, introduces custom error types, error wrapping and chaining introduced in Go 1.13, outlines best‑practice guidelines, and reviews popular libraries and centralised patterns.

Golang Shines
Golang Shines
Golang Shines
Go Error Handling Best Practices: From Basics to Advanced

Effective error handling is essential for building reliable and maintainable software; in Go, errors are represented by the error interface and returned explicitly from functions, which makes error flow clear but requires disciplined handling.

Basic error handling

Errors are defined by implementing the Error() string method:

type error interface {
    Error() string
}

Functions return an error value alongside normal results:

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

Callers must check the returned error before using the result:

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

Advanced error handling

For complex scenarios, define custom error types to carry additional context:

type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}

func processRequest(id int) error {
    if id <= 0 {
        return &AppError{Code: 400, Message: "Invalid ID"}
    }
    // process request …
    return nil
}

Go 1.13 introduced error wrapping with the %w verb, allowing errors to carry a chain of context:

func readFile(filename string) error {
    err := openFile(filename)
    if err != nil {
        return fmt.Errorf("failed to read file %s: %w", filename, err)
    }
    return nil
}

func openFile(filename string) error {
    return errors.New("file not found")
}

Wrapped errors can be inspected with errors.Is and errors.Unwrap to locate the root cause:

err := readFile("nonexistent.txt")
if err != nil {
    fmt.Println("Error:", err)
    var fileErr error = errors.New("file not found")
    if errors.Is(err, fileErr) {
        fmt.Println("This is a file not found error")
    }
}

Best‑practice guidelines

Never ignore errors; always handle or propagate them.

Return errors as soon as they are detected to avoid unnecessary work.

Provide clear, specific error messages that include relevant data.

Use fmt.Errorf with %w to add context while preserving the original error.

Distinguish between recoverable and unrecoverable errors—log and continue for the former, terminate or abort for the latter.

Error‑handling libraries

pkg/errors offers stack traces and richer wrapping:

import "github.com/pkg/errors"

func readFile(filename string) error {
    return errors.New("file not found")
}

func process() error {
    err := readFile("config.json")
    if err != nil {
        return errors.Wrap(err, "process failed")
    }
    return nil
}

func main() {
    if err := process(); err != nil {
        fmt.Println("Error:", err)
        fmt.Println("Stack trace:", errors.WithStack(err))
    }
}

xerrors (standardised in Go 1.13) provides similar wrapping capabilities:

import "golang.org/x/xerrors"

func readFile(filename string) error {
    return xerrors.New("file not found")
}

func process() error {
    err := readFile("config.json")
    if err != nil {
        return xerrors.Errorf("process failed: %w", err)
    }
    return nil
}

func main() {
    if err := process(); err != nil {
        fmt.Println("Error:", err)
        fmt.Println("Root cause:", xerrors.Unwrap(err))
    }
}

Error‑handling patterns

A centralised error handler can map error types to HTTP status codes:

func handleError(w http.ResponseWriter, err error) {
    switch e := err.(type) {
    case *AppError:
        http.Error(w, e.Message, e.Code)
    default:
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    if err := processRequest(r); err != nil {
        handleError(w, err)
        return
    }
    w.WriteHeader(http.StatusOK)
    fmt.Fprint(w, "Success")
}

Logging errors with context aids debugging:

func process() error {
    // processing …
    return errors.New("process failed")
}

func main() {
    if err := process(); err != nil {
        log.Printf("Error: %v", err)
    }
}

Conclusion

Go’s explicit error model encourages clear, controllable error handling. By mastering basic patterns, custom error types, the Go 1.13 wrapping features, and using libraries such as pkg/errors or xerrors , developers can write Go code that is both robust and easy to maintain.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoBest Practiceserror handlingerror wrappingcustom errorpkg/errorsxerrors
Golang Shines
Written by

Golang Shines

We share daily the latest Golang technical articles, practical resources, language news, tutorials, and real-world projects to help everyone learn and improve.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.