Backend Development 17 min read

Error Handling and Errgroup Usage in Go

This article explains Go's built-in error interface, distinguishes errors from exceptions, compares three common error-handling patterns, discusses wrapping errors with packages like pkg/errors, and demonstrates centralized error handling using errgroup, including usage examples and best-practice recommendations for backend Go development.

High Availability Architecture
High Availability Architecture
High Availability Architecture
Error Handling and Errgroup Usage in Go

Go provides a built-in error interface as the standard way to represent errors. Errors are expected problems such as database connection failures, while exceptions are unexpected conditions like nil pointer dereferences or out-of-bounds accesses.

1. Error vs Exception vs panic

An error represents a recoverable problem that should be handled as part of business logic. An exception is an unexpected condition that triggers a panic , causing the program to abort unless recovered.

In Go, panic should be reserved for truly unrecoverable situations (e.g., configuration parsing failure, port binding error). Business code should avoid calling panic directly; instead, use error returns.

2. Three Common Go Error‑Handling Approaches

2.1 Classic Go Logic (return error)

type ZooTour interface {
    Enter() error
    VisitPanda(p *Panda) error
    Leave() error
}

func Tour(t ZooTour, p *Panda) error {
    if err := t.Enter(); err != nil {
        return errors.WithMessage(err, "Enter failed.")
    }
    if err := t.VisitPanda(p); err != nil {
        return errors.WithMessage(err, "VisitPanda failed.")
    }
    // ...
    return nil
}

2.2 Shielding Errors (store error internally)

Objects can keep an internal error state and expose an Err() error method, allowing callers to invoke a sequence of operations without checking each step individually.

type ZooTour interface {
    Enter() error
    VisitPanda(p *Panda) error
    Leave() error
    Err() error
}

func Tour(t ZooTour, p *Panda) error {
    t.Enter()
    t.VisitPanda(p)
    t.Leave()
    if err := t.Err(); err != nil {
        return errors.WithMessage(err, "ZooTour failed")
    }
    return nil
}

2.3 Functional Programming (deferred execution)

Define a Walker that yields functions to be executed in a specific order (e.g., sequential, reverse, tree traversal). This separates the control flow from the data.

type Walker interface { Next MyFunc }

type SliceWalker struct { index int; funs []MyFunc }

func BreakOnError(t ZooTour, w Walker) error {
    for {
        f := w.Next()
        if f == nil { break }
        if err := f(t); err != nil { return err }
    }
    return nil
}

3. Layered Error Handling in a Typical DAO‑Service‑Controller Architecture

Errors are wrapped at each layer to add context while preserving the original cause.

// controller
if err := mode.ParamCheck(param); err != nil {
    log.Errorf("param=%+v", param)
    return errs.ErrInvalidParam
}
return mode.ListTestName("")

// service
_, err := dao.GetTestName(ctx, settleId)
if err != nil {
    log.Errorf("GetTestName failed. err: %v", err)
    return errs.ErrDatabase
}

// dao (using pkg/errors)
if err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        return nil, errors.Wrapf(ierror.ErrNotFound, "query:%s", query)
    }
    return nil, errors.Wrapf(ierror.ErrDatabase, "query: %s error(%v)", query, err)
}

4. Wrapping Errors with github.com/pkg/errors

The package provides functions to create and enrich errors while preserving stack traces.

// Create a new error with stack trace
func New(message string) error

// Add a message
func WithMessage(err error, message string) error

// Add stack trace only
func WithStack(err error) error

// Add both message and stack trace
func Wrapf(err error, format string, args ...interface{}) error

// Retrieve the root cause
func Cause(err error) error

Typical usage in the three‑layer example:

// DAO layer
return nil, errors.Wrapf(ierror.ErrDatabase, "query: %s error(%v)", query, err)

// Service layer
return result, errors.WithMessage(err, "GetName failed")

5. Centralized Error Handling with errgroup

The standard golang.org/x/sync/errgroup provides a Group that runs multiple goroutines and returns the first error.

type Group struct {
    // methods
    func WithContext(ctx context.Context) (*Group, context.Context)
    func (g *Group) Go(f func() error)
    func (g *Group) Wait() error
}

Typical usage:

func TestErrgroup() {
    eg, ctx := errgroup.WithContext(context.Background())
    for i := 0; i < 100; i++ {
        i := i
        eg.Go(func() error {
            time.Sleep(2 * time.Second)
            select {
            case <-ctx.Done():
                fmt.Println("Canceled:", i)
                return nil
            default:
                fmt.Println("End:", i)
                return nil
            }
        })
    }
    if err := eg.Wait(); err != nil {
        log.Fatal(err)
    }
}

6. Bilibili’s Errgroup Extension

The extended version adds explicit concurrency control, panic recovery, and a task queue.

type Group struct {
    err     error
    wg      sync.WaitGroup
    errOnce sync.Once
    workerOnce sync.Once
    ch      chan func(context.Context) error
    chs     []func(context.Context) error
    ctx     context.Context
    cancel  func()
}

func WithContext(ctx context.Context) *Group { return &Group{ctx: ctx} }

func (g *Group) Go(f func(context.Context) error) {
    g.wg.Add(1)
    if g.ch != nil {
        select { case g.ch <- f: default: g.chs = append(g.chs, f) }
        return
    }
    go g.do(f)
}

func (g *Group) GOMAXPROCS(n int) {
    if n <= 0 { panic("errgroup: GOMAXPROCS must greater than 0") }
    g.workerOnce.Do(func() {
        g.ch = make(chan func(context.Context) error, n)
        for i := 0; i < n; i++ {
            go func() { for f := range g.ch { g.do(f) } }()
        }
    })
}

The extension solves three pain points of the standard library: limiting concurrency, recovering from panics with stack traces, and avoiding deadlocks when many tasks are submitted.

7. Tips and Best Practices

Use errors.Wrap or Wrapf when collaborating with other libraries to preserve stack information.

Log errors with %+v to include stack traces; use appropriate log levels (error for stack, warn/info without).

When using errgroup, avoid passing the parent context downstream after cancellation, as it may cause unexpected behavior.

Be cautious with the Bilibili extension’s internal slice; it is not thread‑safe and may cause deadlocks under high concurrency.

8. Author

Li Senlin – Backend Engineer at Tencent, responsible for design, development, and maintenance of the Tencent Game Content Platform.

9. References

喜马拉雅基于Apache ShardingSphere实践

PHP优秀框架Laravel和Yii大PK

砥砺前行 | Kratos 框架 v2 版本架构演进之路

B+树数据库加锁历史

前端工程化之FaaS SSR方案

BackendConcurrencyGoError Handlingerrgrouppkg/errors
High Availability Architecture
Written by

High Availability Architecture

Official account for High Availability Architecture.

0 followers
Reader feedback

How this landed with the community

login 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.