Go error
last modified October 30, 2026
In this article we show how to handle errors in Go. An error is an ordinary
value of the built-in error type: it can be returned, stored,
compared and wrapped. A nil error means that the operation
succeeded.
The examples were compiled and run with Go 1.27.0. Programs that report
expected errors with fmt.Println produce stable output; a real
log.Fatal would add a timestamp.
Errors are values
A function that can fail returns an error as its last result. The caller checks it immediately, before using the other results.
package main
import (
"fmt"
"os"
)
func main() {
data, err := os.ReadFile("thermopylae.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Print(string(data))
_, err = os.ReadFile("missing.txt")
fmt.Println(err)
}
os.ReadFile returns the contents and an error. The successful call
leaves the error nil, while the missing file produces an error that includes
the operation and the path.
$ go run main.go The Battle of Thermopylae was fought in 480 BC. open missing.txt: no such file or directory
Creating errors
errors.New creates a new error with a fixed message. Two calls with
the same text return two distinct values, so an error is usually compared with
a package-level sentinel variable rather than with a new value.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("record not found")
func main() {
err := ErrNotFound
fmt.Println(err)
fmt.Println(err == ErrNotFound)
a := errors.New("boom")
b := errors.New("boom")
fmt.Println(a == b)
fmt.Println(a.Error() == b.Error())
}
The sentinel compares equal to itself. The two separately created errors have the same message but different identities, so only their text is equal.
$ go run main.go record not found true false true
Wrapping errors
fmt.Errorf with the %w verb creates a new error that
wraps the original one. errors.Is walks the chain, while
%v only copies the text and loses the link.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("record not found")
func main() {
wrapped := fmt.Errorf("lookup: %w", ErrNotFound)
plain := fmt.Errorf("lookup: %v", ErrNotFound)
fmt.Println(wrapped)
fmt.Println("wrapped is:", errors.Is(wrapped, ErrNotFound))
fmt.Println("plain is: ", errors.Is(plain, ErrNotFound))
}
Both values print the same text. Only the wrapped one still answers true to
errors.Is, because the original error is part of its chain.
$ go run main.go lookup: record not found wrapped is: true plain is: false
Unwrapping a chain
A sentinel can be wrapped with additional context at each level. The message grows, but the identity remains reachable.
package main
import (
"errors"
"fmt"
"os"
)
func loadConfig() error {
_, err := os.Open("config.yaml")
return fmt.Errorf("load config: %w", err)
}
func start() error {
return fmt.Errorf("start server: %w", loadConfig())
}
func main() {
err := start()
fmt.Println(err)
fmt.Println("not exist:", errors.Is(err, os.ErrNotExist))
fmt.Println("unwrapped:", errors.Unwrap(err))
}
errors.Is(err, os.ErrNotExist) succeeds even though the
fs.PathError from os.Open is two wrappers deep.
errors.Unwrap removes only the outermost layer.
$ go run main.go start server: load config: open config.yaml: no such file or directory not exist: true unwrapped: load config: open config.yaml: no such file or directory
Custom error types
A type implements the error interface by providing an Error
method. A pointer receiver is the usual choice because it avoids copying the
struct, and errors.As still recovers the concrete value together
with its fields.
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Value string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid %s: %q", e.Field, e.Value)
}
func createUser(name string) error {
if name == "" {
return &ValidationError{Field: "name", Value: name}
}
return nil
}
func main() {
err := fmt.Errorf("create user: %w", createUser(""))
fmt.Println(err)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("field:", ve.Field)
fmt.Println("value:", ve.Value)
}
}
The error is wrapped with context before it is inspected.
errors.As takes a pointer to a variable of the target type and
walks the chain until it finds a matching value.
$ go run main.go create user: invalid name: "" field: name value:
Joining errors
errors.Join combines several errors into one. The result prints
each error on its own line and satisfies errors.Is for every
member of the group.
package main
import (
"errors"
"fmt"
)
func main() {
dbErr := errors.New("database unavailable")
cacheErr := fmt.Errorf("cache: %w", errors.New("timeout"))
err := errors.Join(dbErr, cacheErr)
fmt.Println(err)
fmt.Println("db: ", errors.Is(err, dbErr))
fmt.Println("cache:", errors.Is(err, errors.Unwrap(cacheErr)))
}
Joining is useful when an operation has several independent failures, for example when shutting down several components and collecting every problem instead of returning after the first one.
$ go run main.go database unavailable cache: timeout db: true cache: true
Panics and recover
A panic unwinds the stack and terminates the program unless a deferred
function calls recover. A recovered panic can be turned into an
ordinary error, but it should not be used for expected failures.
package main
import (
"fmt"
)
func divide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil
}
func main() {
res, err := divide(10, 2)
fmt.Println(res, err)
res, err = divide(10, 0)
fmt.Println(res, err)
fmt.Printf("%T\n", err)
}
The division by zero raises a runtime panic. The deferred function recovers it,
converts it to an error with fmt.Errorf and lets
divide return normally. The result is zero because the assignment
never happened.
$ go run main.go 5 <nil> 0 recovered: runtime error: integer divide by zero *errors.errorString
Source
Go errors package - reference
Error handling and Go
In this article we have worked with errors in Go. We checked errors as values,
created sentinel errors, wrapped them with %w, inspected chains
with errors.Is and errors.Unwrap, recovered typed
information with errors.As, combined failures with
errors.Join and converted a panic into an error with
recover.
Author
List all Go tutorials.