Idiomatic Go Error Handling
Go has no exceptions; errors are values returned alongside the result. This snippet covers the canonical `if err != nil` check, error wrapping with `fmt.Errorf("...: %w", err)` (Go 1.13+) for context, and unwrapping with `errors.Is` / `errors.As` to inspect underlying error types. Get this right and your stack of error returns will read as cleanly as any try/catch.
1,090 views
7
package main
import (
"errors"
"fmt"
"strconv"
)
func parsePositive(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
if n <= 0 {
return 0, errors.New("value must be positive")
}
return n, nil
}
func main() {
for _, s := range []string{"42", "-3", "abc"} {
n, err := parsePositive(s)
if err != nil {
fmt.Printf("%s -> err: %v\n", s, err)
continue
}
fmt.Printf("%s -> ok: %d\n", s, n)
}
}Every fallible operation returns (value, error). The caller checks err != nil immediately and either handles the error or returns it up the stack. There is no exception machinery to short-circuit the stack, which makes control flow obvious at the cost of more typing. errors.New("text") builds a plain error; fmt.Errorf("context: %v", inner) includes the inner error's message but loses the type, while the %w verb (next accordion) preserves the original for unwrapping. Always handle errors at the layer that has enough context to make a meaningful decision; do not blanket-log-and-continue.
package main
import (
"errors"
"fmt"
)
var ErrTooSmall = errors.New("too small")
func deposit(amount int) error {
if amount < 100 {
return fmt.Errorf("deposit %d: %w", amount, ErrTooSmall)
}
return nil
}
func main() {
err := deposit(42)
fmt.Println("raw:", err)
// errors.Is walks the wrap chain to find a matching sentinel.
if errors.Is(err, ErrTooSmall) {
fmt.Println("caller knows the deposit was too small")
}
}fmt.Errorf with the %w verb attaches an inner error to the new one, building a chain. The outer error carries human-readable context ("deposit 42:") while the inner sentinel value is preserved for programmatic inspection. errors.Is(err, target) walks the chain looking for the target value, regardless of how many layers wrap it. This pattern replaces the pre-1.13 ad-hoc convention of comparing strings or exporting wrapper types. Use a small set of exported sentinel errors (ErrNotFound, ErrConflict) plus wrapping for context, and your call sites stay readable while still being introspectable.
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s %s", e.Field, e.Message)
}
func validate(name string) error {
if name == "" {
return fmt.Errorf("validating user: %w",
&ValidationError{Field: "name", Message: "is required"})
}
return nil
}
func main() {
err := validate("")
fmt.Println("raw:", err)
var v *ValidationError
if errors.As(err, &v) {
fmt.Printf("field=%s msg=%s\n", v.Field, v.Message)
}
}When the inner error carries a custom type (with extra fields like Field here), errors.As(err, &target) walks the wrap chain and assigns the first error of the matching type into target. It returns true if it found one, false otherwise. Compared to a type assertion or switch, errors.As works through arbitrary wrap depth. Use it whenever you want to inspect the structured details of an error rather than just whether it equals a sentinel; the typical pattern is one errors.Is for sentinel cases and one errors.As for typed-error cases at API boundaries.
