Backend Development 13 min read

Understanding Go's Error Handling: Design, Best Practices, and Common Pitfalls

This article explains the design of Go's error type, why errors are returned as pointers, compares error handling across languages, discusses panic, wild goroutines, sentinel and custom errors, and presents practical guidelines such as processing errors once, eliminating redundant checks, and wrapping errors for richer context.

政采云技术
政采云技术
政采云技术
Understanding Go's Error Handling: Design, Best Practices, and Common Pitfalls

For Go programmers the ubiquitous if err != nil { doSomething() } pattern raises the question: what exactly is err ? The answer starts with the standard library's errors package, which defines a simple errorString struct and a New function that returns a pointer to that struct.

The built‑in error interface is defined in the builtin package as:

type error interface {
    Error() string
}

Because error is just an interface with a single Error() method, the implementation can be extremely lightweight. Returning a pointer rather than a value prevents accidental equality of distinct error instances, as demonstrated by a small demo where two calls to New("some error") produce different pointers, while returning a value would make them equal.

The article then surveys error‑handling approaches in other languages (C, C++, Java) and shows how Go diverges by using multiple return values instead of exceptions. In Go, the last return value is conventionally an error , and callers must check it before using any other returned data.

Go's panic is distinct from exceptions; a panic should be considered unrecoverable and not expected to be handled by the caller. Nevertheless, in some scenarios developers deliberately recover from a panic to keep a program running, using a combination of error for expected failures and panic for truly exceptional conditions.

Launching a goroutine without any supervision—so‑called “wild goroutine”—means any panic inside it cannot be caught, leading to hidden crashes. The recommended pattern is to use a goroutine pool with a task queue, allowing centralized panic handling and limiting the total number of goroutines.

Languages that rely on try/except introduce hidden control flow because the execution can jump to an exception block at any point. Go's explicit error return eliminates this hidden flow.

Sentinel errors (predefined variables such as io.ErrShortWrite ) expose the error value as part of a package's public API, which can increase coupling. The article advises avoiding sentinel errors when possible.

Custom errors are easy to create in Go. By defining a struct that implements the Error() method, developers can provide richer error types, use type assertions or switch statements to discriminate them, and even embed error codes for enterprise‑level handling.

package main

import "fmt"

func main() {
    err := New("some error")
    switch err.(type) {
    case *errorString:
        fmt.Println("errorString")
    default:
        fmt.Println("not errorString")
    }
    _, ok := err.(*errorString)
    fmt.Println(ok)
}

func New(text string) error {
    return &errorString{text}
}

type errorString struct { s string }

func (e *errorString) Error() string { return e.s }

When building larger systems, the article stresses the principle “process an error only once”. Logging an error at every layer leads to noisy logs and duplicated effort. Instead, propagate the error upward and handle (e.g., log) it at the topmost layer.

Another guideline is to “eliminate errors” by encapsulating error checks inside the package that generates them, exposing a cleaner API to callers. The article shows examples where a struct holds an internal error field and methods update it, allowing the caller to invoke a sequence of operations without repeatedly checking err .

Finally, wrapping errors with additional context (using packages such as github.com/pkg/errors ) creates an error chain that preserves the original cause while adding useful debugging information. A typical pattern is:

_, err := ioutil.ReadAll(r)
if err != nil {
    return errors.Wrap(err, "read failed")
}

type causer interface { Cause() error }

switch err := errors.Cause(err).(type) {
case *MyError:
    // handle specifically
default:
    // unknown error
}

By recording the wrapped error only at the top level, developers obtain a complete trace of the failure without cluttering intermediate code.

References: https://blog.golang.org/errors-are-values https://coolshell.cn/articles/21140.html

BackendGobest practicesError Handlingcustom errorpanicsentinel error
政采云技术
Written by

政采云技术

ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.

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.