Introduction
Go takes a radically different approach to errors compared to languages with exceptions. In Go, errors are values, returned as the last return value from functions. This makes error handling explicit, visible, and impossible to accidentally ignore. The pattern if err != nil is the heartbeat of Go code.
Key Concepts
- Error as Value: Errors are regular values implementing the
errorinterface, not special control flow constructs. - The
errorInterface: A built-in interface with a singleError() stringmethod. - Sentinel Errors: Package-level error variables used for comparison (e.g.,
io.EOF). - Custom Error Types: Structs implementing the
errorinterface that carry additional context.
Real World Context
Explicit error handling is one of Go's most debated features, but it provides enormous value in production. Every error path is visible in the code, making it easy to audit and test. Unlike exceptions, errors cannot silently propagate through call stacks. Teams at Google, Cloudflare, and Docker cite explicit error handling as a key reason their Go services are reliable.
Deep Dive
Functions that can fail return an error as their last value. The caller checks it immediately.
gof, err := os.Open("filename.ext") if err != nil { log.Fatal(err) }
This pattern appears on nearly every line that calls a fallible function.
The error Interface
The built-in error interface is minimal by design.
gotype error interface { Error() string }
Any type with an Error() string method satisfies it.
Creating Errors
The standard library provides two common ways to create errors.
goimport "errors" err := errors.New("something went wrong") import "fmt" err := fmt.Errorf("failed to process item %d", id)
Use errors.New for static messages and fmt.Errorf when you need formatting.
Custom Error Types
For errors that carry structured data, define a custom type.
gotype ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message) }
Callers can use type assertions or errors.As to extract the structured data.
Common Pitfalls
- Ignoring returned errors — Using
_to discard an error (e.g.,result, _ := doThing()) hides failures that may corrupt state later. - Returning a concrete nil pointer as an interface — Returning
(*MyError)(nil)aserrorcreates a non-nil interface, surprising callers who checkerr != nil.
Best Practices
- Always check errors immediately — Handle or return every error right after the call that produces it.
- Add context when propagating — Use
fmt.Errorf("doing X: %w", err)so the final error message tells a complete story.
Summary
- Go treats errors as values, not exceptions.
- The
errorinterface requires only anError() stringmethod. - Create errors with
errors.Neworfmt.Errorf. - Custom error types carry structured context for callers.
- Always check and handle errors immediately after the call.
Code Examples
import "errors"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}