Error Handling Basics
10 examples to get you started with Error Handling - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with Error Handling - 7 basic and 3 intermediate.
mkdir errdemo && cd errdemo && go mod init example.com/errdemo.main.go and run with go run ..Every failure path starts with checking the second return value.
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("missing.txt")
if err != nil {
fmt.Println("open failed:", err)
return
}
defer f.Close()
}(T, error); non-nil error means failure.err != nil, treat the primary result as invalid unless documented otherwise.nil error means success.Related: Errors as Values: Go's Error Philosophy - why Go uses values, not exceptions
Propagate failures by returning error to the caller.
package main
import (
"errors"
"fmt"
)
func parsePort(s string) (int, error) {
if s == "" {
return 0, errors.New("port required")
}
return 8080, nil // simplified
}
func main() {
if _, err := parsePort(""); err != nil {
fmt.Println(err)
}
}errors.New for static messages without formatting.T alongside the error.Related: Sentinel Errors & errors.Is - package-level error variables
Add dynamic detail when the message depends on values.
package main
import (
"fmt"
)
func loadUser(id int) error {
if id < 1 {
return fmt.Errorf("invalid user id %d", id)
}
return nil
}
func main() {
fmt.Println(loadUser(-1))
}fmt.Errorf is like fmt.Sprintf but returns an error.%w when wrapping an existing error (covered in intermediate examples).Related: Error Wrapping with %w & errors.As -
%wand unwrap chains
Avoid deep nesting by returning immediately after a failure.
package main
import (
"fmt"
"os"
)
func readSize(path string) (int64, error) {
info, err := os.Stat(path)
if err != nil {
return 0, err
}
return info.Size(), nil
}
func main() {
size, err := readSize("go.mod")
if err != nil {
fmt.Println(err)
return
}
fmt.Println("bytes:", size)
}err before using the success value.return zero, err inside helpers.if err == nil.Compare stable package-level errors through wrap chains.
package main
import (
"errors"
"fmt"
"io"
"os"
)
func main() {
_, err := os.Open("nope.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file missing")
return
}
if errors.Is(err, io.EOF) {
fmt.Println("unexpected EOF")
}
}errors.Is walks unwrap chains; err == os.ErrNotExist fails after wrapping.os.ErrNotExist and io.EOF.var ErrThing = errors.New("thing").Related: Sentinel Errors & errors.Is - sentinel design rules
Clean up resources even when you return early.
package main
import (
"fmt"
"os"
)
func copyMeta(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// read metadata...
return nil
}
func main() {
fmt.Println(copyMeta("go.mod"))
}defer f.Close() immediately after a successful open.err from the operation that produced f.Pick one primary action per layer unless adding distinct context.
package main
import (
"fmt"
"log"
"os"
)
func main() {
data, err := os.ReadFile("config.yaml")
if err != nil {
log.Printf("read config: %v", err)
return
}
fmt.Printf("loaded %d bytes\n", len(data))
}main or handlers often log and exit or respond.Related: Error Handling Best Practices - library vs application responsibilities
Preserve the root cause while adding context at each layer.
package main
import (
"fmt"
"os"
)
func readConfig(path string) error {
_, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config %q: %w", path, err)
}
return nil
}
func main() {
err := readConfig("missing.yaml")
fmt.Println(err)
fmt.Println(os.IsNotExist(err)) // true through the wrap chain
}%w stores the wrapped error for errors.Is and errors.As.os.IsNotExist and errors.Is both traverse wraps.%v instead of %w when the inner error must not be inspectable.Related: Error Wrapping with %w & errors.As - full wrapping guide
Extract structured data from a custom error type.
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
}
func (e ValidationError) Error() string {
return "invalid field: " + e.Field
}
func validate(name string) error {
if name == "" {
return ValidationError{Field: "name"}
}
return nil
}
func main() {
err := validate("")
var ve ValidationError
if errors.As(err, &ve) {
fmt.Println("field:", ve.Field)
}
}errors.As assigns the first matching type in the unwrap chain.&ve).Related: Custom Error Types & Error Interfaces - designing rich errors
Map domain errors to HTTP status at the boundary.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func getUser(id string) error {
if id == "" {
return ErrNotFound
}
return nil
}
func statusFor(err error) int {
switch {
case errors.Is(err, ErrNotFound):
return 404
case err != nil:
return 500
default:
return 200
}
}
func main() {
fmt.Println(statusFor(getUser(""))) // 404
fmt.Println(statusFor(getUser("1"))) // 200
}errors.Is for sentinel mapping; avoid leaking os or driver strings.Related: API Error Design for HTTP and gRPC Services - service error contracts
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026