Error Values
The error interface
Section titled “The error interface”Go’s error type is the simplest interface in the standard library:
type error interface { Error() string}Any type that implements a single method — Error() string — satisfies this interface and can be used wherever an error is expected. The nil value is a valid error and universally means “no error occurred.” There is nothing special about the error type at the language level; it is an ordinary interface that happens to be predeclared.
Returning errors from functions
Section titled “Returning errors from functions”The idiomatic Go convention is to return (T, error) as the last two return values from any function that can fail. The caller always receives both and must inspect the error before trusting the result:
result, err := riskyOperation()if err != nil { // do not use result — it is meaningless return fmt.Errorf("riskyOperation: %v", err)}// err is nil: result is validReturning the error to the caller is the norm. Swallowing errors silently — ignoring err with _ — should be deliberate and rare, reserved for cases where failure is genuinely irrelevant (e.g., closing a read-only file after all reads succeeded).
Creating errors
Section titled “Creating errors”Go provides two standard ways to create a new error value:
errors.New creates an error from a static string. Use it when the message never changes:
var errTimeout = errors.New("operation timed out")fmt.Errorf creates a formatted error message. Use it when you need to include runtime values like names, keys, or counts:
err := fmt.Errorf("user %d not found in database %q", userID, dbName)Note: %v formats the value inline and does not attach wrapping metadata. Wrapping — using %w to chain errors so errors.Is and errors.As can unwrap the chain — is covered in the next lesson.
Sentinel errors
Section titled “Sentinel errors”A sentinel error is a package-level variable that represents a specific, well-known failure condition:
var ErrNotFound = errors.New("not found")var ErrPermission = errors.New("permission denied")Declaring sentinels at package scope lets callers test for them by identity. Use errors.Is rather than == so the check still works when the error has been wrapped with additional context:
if errors.Is(err, ErrNotFound) { // handle the not-found case}Sentinels work best for conditions that callers need to branch on. Avoid creating a sentinel for every possible failure — reserve them for errors that callers genuinely handle differently from the generic failure case.
The if err != nil pattern
Section titled “The if err != nil pattern”The explicit if err != nil check may feel repetitive at first. It is intentional. Go treats every failure as a decision point: you must choose to propagate the error, wrap it with context, retry, fall back to a default, or log and continue. The language does not make that choice for you.
This discipline produces code where every error-producing call is visible at the callsite. When you read a Go function, you can see exactly which operations can fail and what the caller does about each one — no implicit propagation, no invisible exception paths.
package main
import ( "errors" "fmt")
var ErrNotFound = errors.New("not found")var ErrPermission = errors.New("permission denied")
func lookup(key string) (int, error) { switch key { case "age": return 30, nil case "secret": return 0, ErrPermission default: return 0, fmt.Errorf("lookup %q: %w", key, ErrNotFound) }}
func main() { v, err := lookup("age") if err != nil { fmt.Println("error:", err) return } fmt.Println("age:", v)
_, err = lookup("x") if err != nil { fmt.Println("error:", err) fmt.Println("is ErrNotFound:", errors.Is(err, ErrNotFound)) }
_, err = lookup("secret") if err != nil { fmt.Println("error:", err) fmt.Println("is ErrPermission:", errors.Is(err, ErrPermission)) }}Loading Go runtime (first run only, ~8 MB)…