Error Handling Basics
10 examples to get you started with Error Handling - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir errdemo && cd errdemo && go mod init example.com/errdemo. - Save snippets as
main.goand run withgo run ..
Basic Examples
1. The error interface and nil check
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()
}- Functions that can fail return
(T, error); non-nilerrormeans failure. - When
err != nil, treat the primary result as invalid unless documented otherwise. nilerror means success.
Related: Errors as Values: Go's Error Philosophy - why Go uses values, not exceptions
2. Returning errors from your own functions
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)
}
}- Use
errors.Newfor static messages without formatting. - Return the zero value for
Talongside the error. - Callers decide whether to wrap, log, or abort.
Related: Sentinel Errors & errors.Is - package-level error variables
3. fmt.Errorf for formatted errors
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.Errorfis likefmt.Sprintfbut returns anerror.- Prefer
%wwhen wrapping an existing error (covered in intermediate examples). - Keep messages actionable: what failed and with what input.
Related: Error Wrapping with %w & errors.As -
%wand unwrap chains
4. Early return on error
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)
}- Check
errbefore using the success value. - Propagate with
return zero, errinside helpers. - Flat control flow is easier to read than nested
if err == nil.
5. Sentinel error with errors.Is
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.Iswalks unwrap chains;err == os.ErrNotExistfails after wrapping.- Standard library exports sentinels like
os.ErrNotExistandio.EOF. - Define your own with
var ErrThing = errors.New("thing").
Related: Sentinel Errors & errors.Is - sentinel design rules
6. Defer still runs on error paths
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"))
}- Register
defer f.Close()immediately after a successful open. - Deferred calls run when the function exits, including on error returns.
- Do not defer before checking
errfrom the operation that producedf.
7. Log or return, not both blindly
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))
}- Libraries return errors;
mainor handlers often log and exit or respond. - Duplicate logging at every layer creates noisy, correlated log lines.
- Wrap before return when upstream needs context.
Related: Error Handling Best Practices - library vs application responsibilities
Intermediate Examples
8. Wrap errors with %w
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
}%wstores the wrapped error forerrors.Isanderrors.As.os.IsNotExistanderrors.Isboth traverse wraps.- Use
%vinstead of%wwhen the inner error must not be inspectable.
Related: Error Wrapping with %w & errors.As - full wrapping guide
9. Typed errors with errors.As
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.Asassigns the first matching type in the unwrap chain.- Pass a pointer to the target type (
&ve). - Custom types enable fields without string parsing.
Related: Custom Error Types & Error Interfaces - designing rich errors
10. Handle errors in a small HTTP-style handler
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
}- Translate errors to client-safe messages and status codes at the edge.
- Use
errors.Isfor sentinel mapping; avoid leakingosor driver strings. - Internal details belong in logs, not response bodies.
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).