Error Handling And Best Practices (Dealing With Errors, Panics, Recover)
Overview
Go’s approach to error handling is simple but powerful, emphasizing clear control flow over error-prone constructs like try/catch. In this chapter, you will learn how to effectively manage errors in Go using idiomatic patterns. The chapter covers basic error handling, creating and returning errors, and best practices such as wrapping errors for better context. You will also explore advanced topics like custom error types, sentinel errors, error chain traversal, modern error inspection with errors.Is() and the generic errors.AsType() added in Go 1.26, custom unmarshal error handling, and the use of Go’s panic and recover for managing unexpected failures. By the end, you’ll have a deep understanding of how Go’s error model leads to more reliable and maintainable code, while learning when and how to use recovery mechanisms for graceful error handling.
No Try/Catch
In Go there is no try/catch paradigm. Errors are return values that need to be handled like any other value returned from a function call.
package main
import (
"log"
"os"
)
func main() {
_, err := os.Open("/path/to/some/content.file")
if err != nil {
// open /path/to/some/content.file: no such file or directory
log.Fatal(err)
}
}package main
import (
"log"
"os"
)
func main() {
_, err := os.Open("/path/to/some/content.file")
if err != nil {
// open /path/to/some/content.file: no such file or directory
log.Fatal(err)
}
}If the err is nil, then you know the call succeeded, otherwise something went wrong.
Gracefully Handle Errors
Because errors are values, we can handle them gracefully in our programs.
In this example, we look to see if a config file was provided by the user. If not, we’ll fall back to a default config.
This is an example of even though we received an error, we could gracefully recover and plan for an alternative action.
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
var f io.Reader
var err error
// try to read a file
f, err = os.Open("./config.json")
if err != nil {
// Fall back to a default config. Stream it as a reader.
f = strings.NewReader(`{"backup":false,"debug":true}`)
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"strings"
)
func main() {
// section: code
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
var f io.Reader
var err error
// try to read a file
f, err = os.Open("./config.json")
if err != nil {
// Fall back to a default config. Stream it as a reader.
f = strings.NewReader(`{"backup":false,"debug":true}`)
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
// section: code
}
/*
// section: output
{Backup:false Debug:true}
// section: output
*/Output:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"strings"
)
func main() {
// section: code
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
var f io.Reader
var err error
// try to read a file
f, err = os.Open("./config.json")
if err != nil {
// Fall back to a default config. Stream it as a reader.
f = strings.NewReader(`{"backup":false,"debug":true}`)
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
// section: code
}
/*
// section: output
{Backup:false Debug:true}
// section: output
*/Creating Errors
There are two “built-in” ways to create errors in the standard libary.
// in the `errors` package
errors.New("a message")
// in the `fmt` package
fmt.Errorf("a %s message", "formatted")
Returning Errors
Errors are returned from functions just like any other type.
func boom() error {
return errors.New("boom!")
}
func greetOrBoom() (string, error) {
return "hello", errors.New("boom!")
}
Sentinel Errors
Sentinel errors use a specific value to signify that no further processing is possible.
Examples in the standard library include io.EOF and sql.ErrNoRows.
io.EOF indicates that you have reached the end of a file, which isn’t really an error; we expect it to happen when we want to read the entire file.
Error values are just variables:
// EOF is returned when the end of the file has
// been reached.
var EOF = errors.New("EOF")
Sentinel Example
In this code, we are opening a file and reading the contents by using the io.Reader interface. We expect to see an io.EOF error. We use this sentinel error to signal that we have completed our task successfully.
func main() {
f, err := os.Open("./README.md")
if err != nil {
log.Fatal(err)
}
buff := make([]byte, 10)
for {
i, err := f.Read(buff)
if err != nil {
// Check for the sentinel error of `io.EOF`
if err == io.EOF {
return
}
log.Fatal(err)
}
if _, err := os.Stdout.Write(buff[:i]); err != nil {
log.Fatal(err)
}
}
}
package main
import (
"io"
"log"
"os"
)
// section: code
func main() {
f, err := os.Open("./README.md")
if err != nil {
log.Fatal(err)
}
buff := make([]byte, 10)
for {
i, err := f.Read(buff)
if err != nil {
// Check for the sentinel error of `io.EOF`
if err == io.EOF {
return
}
log.Fatal(err)
}
if _, err := os.Stdout.Write(buff[:i]); err != nil {
log.Fatal(err)
}
}
}
// section: codeCustom Sentinel Errors
You can also create your own sentinel errors.
var errConfigNotFound = errors.New("config not found")
func main() {
c, err := openConfig("./config.json")
if err != nil {
if err == errConfigNotFound {
fmt.Println("using default config")
// ... load default config, etc.
return
}
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
f, err := os.Open(path)
if err != nil {
// Return our own sentinel error
return nil, errConfigNotFound
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
)
// section: code
var errConfigNotFound = errors.New("config not found")
func main() {
c, err := openConfig("./config.json")
if err != nil {
if err == errConfigNotFound {
fmt.Println("using default config")
// ... load default config, etc.
return
}
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
f, err := os.Open(path)
if err != nil {
// Return our own sentinel error
return nil, errConfigNotFound
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
// section: code
/*
// section: output
using default config
// section: output
*/Output:
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
)
// section: code
var errConfigNotFound = errors.New("config not found")
func main() {
c, err := openConfig("./config.json")
if err != nil {
if err == errConfigNotFound {
fmt.Println("using default config")
// ... load default config, etc.
return
}
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
f, err := os.Open(path)
if err != nil {
// Return our own sentinel error
return nil, errConfigNotFound
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
// section: code
/*
// section: output
using default config
// section: output
*/Errors Are Interfaces
Errors in Go are defined as an interface in the standard library.
type error interface {
Error() string
}
Anything that defines this method can now satisfy that interface, and as such, be an error.
Defining Your Error
You can create even a simple custom error as long as you satisfy the error interface:
type strError string
func (s strError) Error() string {
return string(s)
}
func main() {
err := boom()
if err != nil {
log.Fatal(err)
}
}
func boom() error {
return strError("something went wrong")
}
package main
import "log"
// section: code
type strError string
func (s strError) Error() string {
return string(s)
}
func main() {
err := boom()
if err != nil {
log.Fatal(err)
}
}
func boom() error {
return strError("something went wrong")
}
// section: code
/*
// section: output
2020/04/18 13:55:40 something went wrong
exit status 1
// section: output
*/Output:
package main
import "log"
// section: code
type strError string
func (s strError) Error() string {
return string(s)
}
func main() {
err := boom()
if err != nil {
log.Fatal(err)
}
}
func boom() error {
return strError("something went wrong")
}
// section: code
/*
// section: output
2020/04/18 13:55:40 something went wrong
exit status 1
// section: output
*/Complex Custom Error
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func boom() error {
return ComplexError{
File: "main.go",
Server: "myhost",
}
}
package main
import (
"fmt"
"log"
)
// section: error
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func boom() error {
return ComplexError{
File: "main.go",
Server: "myhost",
}
}
// section: error
// section: main
func main() {
err := boom()
if err != nil {
log.Println(err)
}
if ce, ok := err.(ComplexError); ok {
log.Println(ce.Server)
}
}
// section: mainUsing Type Assertion On Errors
Using type assertion, you can get access to the concrete type and therefore access the fields and methods of the concrete type.
func main() {
err := boom()
if err != nil {
log.Println(err)
}
if ce, ok := err.(ComplexError); ok {
log.Println(ce.Server)
}
}
package main
import (
"fmt"
"log"
)
// section: error
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func boom() error {
return ComplexError{
File: "main.go",
Server: "myhost",
}
}
// section: error
// section: main
func main() {
err := boom()
if err != nil {
log.Println(err)
}
if ce, ok := err.(ComplexError); ok {
log.Println(ce.Server)
}
}
// section: mainComparing Errors: Why `==` Falls Short
Each call to errors.New returns a new, distinct error value. Two errors created from the exact same message are therefore not equal under ==:
var err3 = errors.New("EOF")
var err4 = errors.New("EOF")
fmt.Printf("err3 == err4: %v\n", err3 == err4) // falsepackage main
import (
"errors"
"fmt"
)
// section: customerror
type Error string
func (e Error) Error() string {
return string(e)
}
// section: customerror
func main() {
// section: const
const str1 = "EOF"
const str2 = "EOF"
fmt.Printf("str1 == str2: %v\n", str1 == str2) // true
// section: const
// section: consterr
const err1 = Error("EOF")
const err2 = Error("EOF")
fmt.Printf("err1 == err2: %v\n", err1 == err2) // true
// section: consterr
// section: varerr
var err3 = errors.New("EOF")
var err4 = errors.New("EOF")
fmt.Printf("err3 == err4: %v\n", err3 == err4) // false
// section: varerr
}The fix isn’t just a different operator: comparing these two values with errors.Is is false too, because neither one is derived from the other. errors.Is does not compare error messages. Instead, compare against the actual sentinel your code returns or wraps. Define it once (for example var ErrNotFound = errors.New("not found")) and check for it with errors.Is, which also walks the chain of wrapped errors and honors an Is method. (We cover errors.Is in detail later in this module.)
Advanced: Because Go constants are comparable by value, you can base an error on a string type and declare sentinel errors as
const. Constant errors are value-comparable and can’t be reassigned by another package at runtime. It’s a niche technique, worth knowing but rarely needed.
Constant Errors - Dave Cheney on immutable, value-comparable sentinel errors.
Sentinel Errors Best Practices
Despite a few use cases in the standard library, it is generally considered to be “best practice” to carefully manage sentinel errors.
Traditional approach (less flexible):
- Sentinel errors become part of your public API
- Sentinel errors create a dependency between two packages
Modern approach (recommended):
- Keep error types unexported (lowercase) to maintain API flexibility
- Provide predicate functions (like
IsNotFound(err error) bool) as your public API - Use
errors.AsType()inside predicate functions to check for unexported types (errors.As()before Go 1.26) - Never add
Is()method to error types (it becomes part of your API contract forever) - Use constant errors for truly immutable values
A great example of this is the os.IsExist method in the standard library. This will allow you better control over how this API is used vs. declaring a sentinel error.
func IsExist(err error) bool
// Modern best practices for sentinel errors and error checking
// ============================================
// PATTERN 1: Exported Error Variables (Traditional)
// ============================================
// Use for well-known error conditions in public APIs
var (
// ErrNotFound indicates a resource was not found
ErrNotFound = errors.New("not found")
// ErrInvalidInput indicates invalid input was provided
ErrInvalidInput = errors.New("invalid input")
// ErrTimeout indicates an operation timed out
ErrTimeout = errors.New("operation timed out")
)
// ============================================
// PATTERN 2: Unexported Error Types (RECOMMENDED)
// ============================================
// Keep error types unexported to maintain API flexibility
// Use predicate functions to check for error conditions
// notFoundError is unexported - implementation detail
type notFoundError struct {
Resource string
ID string
}
func (e *notFoundError) Error() string {
return fmt.Sprintf("%s not found: %s", e.Resource, e.ID)
}
// IMPORTANT: No Is() method on the error type!
// Adding Is() to the type makes it part of your API contract forever.
// You can't remove it without breaking changes.
// ============================================
// PATTERN 3: Predicate Functions (BEST PRACTICE)
// ============================================
// Predicate functions give you flexibility to change implementation
// IsNotFound checks if an error represents a "not found" condition
// This is the public API - you can change the implementation anytime
func IsNotFound(err error) bool {
// Check for sentinel error
if errors.Is(err, ErrNotFound) {
return true
}
// Check for unexported custom type using errors.AsType
_, ok := errors.AsType[*notFoundError](err)
return ok
}
// IsTimeout checks if an error represents a timeout condition
func IsTimeout(err error) bool {
return errors.Is(err, ErrTimeout)
}
// IsTemporary checks if an error is temporary (like network errors)
// This follows the pattern used by net.Error
type temporary interface {
error
Temporary() bool
}
func IsTemporary(err error) bool {
t, ok := errors.AsType[temporary](err)
return ok && t.Temporary()
}
// ============================================
// PATTERN 4: Wrapping Sentinel Errors
// ============================================
// findUser simulates a database lookup
func findUser(id string) error {
// Simulate not found - return unexported error type
if id == "" {
return ¬FoundError{Resource: "user", ID: id}
}
return nil
}
// getUserProfile demonstrates proper error wrapping with sentinels
func getUserProfile(userID string) error {
err := findUser(userID)
if err != nil {
// Wrap the error while preserving the sentinel
return fmt.Errorf("failed to get user profile: %w", err)
}
return nil
}
// ============================================
// PATTERN 5: Constant Errors
// ============================================
// For truly immutable error values
type constError string
func (e constError) Error() string {
return string(e)
}
const (
ErrInvalidFormat constError = "invalid format"
ErrOutOfRange constError = "value out of range"
)
// ============================================
// EXAMPLES
// ============================================
func demonstrateSentinelErrors() {
fmt.Printf("=== Sentinel Error Patterns ===\n\n")
// Example 1: Traditional sentinel error
fmt.Println("1. Traditional Sentinel Error:")
err1 := ErrNotFound
if errors.Is(err1, ErrNotFound) {
fmt.Printf(" ✓ Matched sentinel error: %v\n", err1)
}
// Example 2: Wrapped sentinel error
fmt.Println("\n2. Wrapped Sentinel Error:")
err2 := fmt.Errorf("operation failed: %w", ErrTimeout)
if errors.Is(err2, ErrTimeout) {
fmt.Printf(" ✓ Matched wrapped sentinel: %v\n", err2)
}
// Example 3: Unexported custom error type via predicate
fmt.Println("\n3. Unexported Custom Error Type:")
err3 := ¬FoundError{Resource: "user", ID: "123"}
if IsNotFound(err3) {
fmt.Printf(" ✓ Matched via predicate function: %v\n", err3)
}
// Example 4: Wrapped custom error
fmt.Println("\n4. Wrapped Custom Error:")
err4 := getUserProfile("")
if IsNotFound(err4) {
fmt.Printf(" ✓ Matched wrapped custom error: %v\n", err4)
}
// Example 5: Standard library errors
fmt.Println("\n5. Standard Library Errors:")
err5 := io.EOF
if errors.Is(err5, io.EOF) {
fmt.Printf(" ✓ Matched io.EOF: %v\n", err5)
}
err6 := fs.ErrNotExist
if errors.Is(err6, fs.ErrNotExist) {
fmt.Printf(" ✓ Matched fs.ErrNotExist: %v\n", err6)
}
// Example 6: Constant errors
fmt.Println("\n6. Constant Errors:")
err7 := ErrInvalidFormat
if err7 == ErrInvalidFormat {
fmt.Printf(" ✓ Matched constant error: %v\n", err7)
}
}
// ============================================
// BEST PRACTICES SUMMARY
// ============================================
func main() {
demonstrateSentinelErrors()
fmt.Println("\n=== Best Practices ===")
fmt.Println("✓ Use predicate functions (IsNotFound) instead of exposing sentinels")
fmt.Println("✓ Keep error types UNEXPORTED to maintain API flexibility")
fmt.Println("✓ Always wrap errors with %w to preserve error chains")
fmt.Println("✓ Use errors.Is() for sentinel error comparison")
fmt.Println("✓ Use errors.AsType[T]() inside predicate functions for type checking (errors.As() before Go 1.26)")
fmt.Println("✓ Consider constant errors for truly immutable values")
fmt.Println("✗ NEVER add Is() method to error types (locks you into the API forever)")
fmt.Println("✗ NEVER export error types (prevents deprecation and changes)")
fmt.Println("✗ Avoid direct equality checks (err == ErrNotFound) on wrapped errors")
}
package main
import (
"errors"
"fmt"
"io"
"io/fs"
)
// section: code
// Modern best practices for sentinel errors and error checking
// ============================================
// PATTERN 1: Exported Error Variables (Traditional)
// ============================================
// Use for well-known error conditions in public APIs
var (
// ErrNotFound indicates a resource was not found
ErrNotFound = errors.New("not found")
// ErrInvalidInput indicates invalid input was provided
ErrInvalidInput = errors.New("invalid input")
// ErrTimeout indicates an operation timed out
ErrTimeout = errors.New("operation timed out")
)
// ============================================
// PATTERN 2: Unexported Error Types (RECOMMENDED)
// ============================================
// Keep error types unexported to maintain API flexibility
// Use predicate functions to check for error conditions
// notFoundError is unexported - implementation detail
type notFoundError struct {
Resource string
ID string
}
func (e *notFoundError) Error() string {
return fmt.Sprintf("%s not found: %s", e.Resource, e.ID)
}
// IMPORTANT: No Is() method on the error type!
// Adding Is() to the type makes it part of your API contract forever.
// You can't remove it without breaking changes.
// ============================================
// PATTERN 3: Predicate Functions (BEST PRACTICE)
// ============================================
// Predicate functions give you flexibility to change implementation
// IsNotFound checks if an error represents a "not found" condition
// This is the public API - you can change the implementation anytime
func IsNotFound(err error) bool {
// Check for sentinel error
if errors.Is(err, ErrNotFound) {
return true
}
// Check for unexported custom type using errors.AsType
_, ok := errors.AsType[*notFoundError](err)
return ok
}
// IsTimeout checks if an error represents a timeout condition
func IsTimeout(err error) bool {
return errors.Is(err, ErrTimeout)
}
// IsTemporary checks if an error is temporary (like network errors)
// This follows the pattern used by net.Error
type temporary interface {
error
Temporary() bool
}
func IsTemporary(err error) bool {
t, ok := errors.AsType[temporary](err)
return ok && t.Temporary()
}
// ============================================
// PATTERN 4: Wrapping Sentinel Errors
// ============================================
// findUser simulates a database lookup
func findUser(id string) error {
// Simulate not found - return unexported error type
if id == "" {
return ¬FoundError{Resource: "user", ID: id}
}
return nil
}
// getUserProfile demonstrates proper error wrapping with sentinels
func getUserProfile(userID string) error {
err := findUser(userID)
if err != nil {
// Wrap the error while preserving the sentinel
return fmt.Errorf("failed to get user profile: %w", err)
}
return nil
}
// ============================================
// PATTERN 5: Constant Errors
// ============================================
// For truly immutable error values
type constError string
func (e constError) Error() string {
return string(e)
}
const (
ErrInvalidFormat constError = "invalid format"
ErrOutOfRange constError = "value out of range"
)
// ============================================
// EXAMPLES
// ============================================
func demonstrateSentinelErrors() {
fmt.Printf("=== Sentinel Error Patterns ===\n\n")
// Example 1: Traditional sentinel error
fmt.Println("1. Traditional Sentinel Error:")
err1 := ErrNotFound
if errors.Is(err1, ErrNotFound) {
fmt.Printf(" ✓ Matched sentinel error: %v\n", err1)
}
// Example 2: Wrapped sentinel error
fmt.Println("\n2. Wrapped Sentinel Error:")
err2 := fmt.Errorf("operation failed: %w", ErrTimeout)
if errors.Is(err2, ErrTimeout) {
fmt.Printf(" ✓ Matched wrapped sentinel: %v\n", err2)
}
// Example 3: Unexported custom error type via predicate
fmt.Println("\n3. Unexported Custom Error Type:")
err3 := ¬FoundError{Resource: "user", ID: "123"}
if IsNotFound(err3) {
fmt.Printf(" ✓ Matched via predicate function: %v\n", err3)
}
// Example 4: Wrapped custom error
fmt.Println("\n4. Wrapped Custom Error:")
err4 := getUserProfile("")
if IsNotFound(err4) {
fmt.Printf(" ✓ Matched wrapped custom error: %v\n", err4)
}
// Example 5: Standard library errors
fmt.Println("\n5. Standard Library Errors:")
err5 := io.EOF
if errors.Is(err5, io.EOF) {
fmt.Printf(" ✓ Matched io.EOF: %v\n", err5)
}
err6 := fs.ErrNotExist
if errors.Is(err6, fs.ErrNotExist) {
fmt.Printf(" ✓ Matched fs.ErrNotExist: %v\n", err6)
}
// Example 6: Constant errors
fmt.Println("\n6. Constant Errors:")
err7 := ErrInvalidFormat
if err7 == ErrInvalidFormat {
fmt.Printf(" ✓ Matched constant error: %v\n", err7)
}
}
// ============================================
// BEST PRACTICES SUMMARY
// ============================================
func main() {
demonstrateSentinelErrors()
fmt.Println("\n=== Best Practices ===")
fmt.Println("✓ Use predicate functions (IsNotFound) instead of exposing sentinels")
fmt.Println("✓ Keep error types UNEXPORTED to maintain API flexibility")
fmt.Println("✓ Always wrap errors with %w to preserve error chains")
fmt.Println("✓ Use errors.Is() for sentinel error comparison")
fmt.Println("✓ Use errors.AsType[T]() inside predicate functions for type checking (errors.As() before Go 1.26)")
fmt.Println("✓ Consider constant errors for truly immutable values")
fmt.Println("✗ NEVER add Is() method to error types (locks you into the API forever)")
fmt.Println("✗ NEVER export error types (prevents deprecation and changes)")
fmt.Println("✗ Avoid direct equality checks (err == ErrNotFound) on wrapped errors")
}
// section: code
/*
// section: output
=== Sentinel Error Patterns ===
1. Traditional Sentinel Error:
✓ Matched sentinel error: not found
2. Wrapped Sentinel Error:
✓ Matched wrapped sentinel: operation failed: operation timed out
3. Unexported Custom Error Type:
✓ Matched via predicate function: user not found: 123
4. Wrapped Custom Error:
✓ Matched wrapped custom error: failed to get user profile: user not found:
5. Standard Library Errors:
✓ Matched io.EOF: EOF
✓ Matched fs.ErrNotExist: file does not exist
6. Constant Errors:
✓ Matched constant error: invalid format
=== Best Practices ===
✓ Use predicate functions (IsNotFound) instead of exposing sentinels
✓ Keep error types UNEXPORTED to maintain API flexibility
✓ Always wrap errors with %w to preserve error chains
✓ Use errors.Is() for sentinel error comparison
✓ Use errors.AsType[T]() inside predicate functions for type checking (errors.As() before Go 1.26)
✓ Consider constant errors for truly immutable values
✗ NEVER add Is() method to error types (locks you into the API forever)
✗ NEVER export error types (prevents deprecation and changes)
✗ Avoid direct equality checks (err == ErrNotFound) on wrapped errors
// section: output
*/Exercise (15 Min)
package main
import (
"fmt"
"log"
)
// #TODO: Declare a struct called `errNotFound`
// - Add an unexported field called `key` of type `string
// #TODO: Implement the error interface for `errNotFound`
// - It should return a string formatted as:
// "couldn't find value for key: <key>"
// hint: you can use the fmt.Sprintf function
func main() {
s := NewStore()
// check to see if key exists
_, err := s.Get("Cory")
// #TODO: Change the following code to check for the specific error of `errNotFound`
// - If it is the `errNotFound`, then only set the value for `Cory`
// - If it is any other error, log.Fatal instead.
if err != nil {
s.Set("Cory", "cory@gopherguides.com")
}
v, err := s.Get("Cory")
if err != nil {
log.Fatal(err)
}
fmt.Println(v)
}
type Store struct {
keys map[string]string
}
func NewStore() *Store {
return &Store{
keys: make(map[string]string),
}
}
func (s *Store) Set(k, v string) {
s.keys[k] = v
}
func (s *Store) Get(k string) (string, error) {
if v, ok := s.keys[k]; ok {
return v, nil
}
// #TODO: return a errNotFound error instead of this generic error
return "", fmt.Errorf("couldn't find value for key: %s", k)
}package main
import (
"fmt"
"log"
)
// #TODO: Declare a struct called `errNotFound`
// - Add an unexported field called `key` of type `string
// #TODO: Implement the error interface for `errNotFound`
// - It should return a string formatted as:
// "couldn't find value for key: <key>"
// hint: you can use the fmt.Sprintf function
func main() {
s := NewStore()
// check to see if key exists
_, err := s.Get("Cory")
// #TODO: Change the following code to check for the specific error of `errNotFound`
// - If it is the `errNotFound`, then only set the value for `Cory`
// - If it is any other error, log.Fatal instead.
if err != nil {
s.Set("Cory", "cory@gopherguides.com")
}
v, err := s.Get("Cory")
if err != nil {
log.Fatal(err)
}
fmt.Println(v)
}
type Store struct {
keys map[string]string
}
func NewStore() *Store {
return &Store{
keys: make(map[string]string),
}
}
func (s *Store) Set(k, v string) {
s.keys[k] = v
}
func (s *Store) Get(k string) (string, error) {
if v, ok := s.keys[k]; ok {
return v, nil
}
// #TODO: return a errNotFound error instead of this generic error
return "", fmt.Errorf("couldn't find value for key: %s", k)
}Solution
package main
import (
"fmt"
"log"
)
type errNotFound struct {
key string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("couldn't find value for key: %s", e.key)
}
func main() {
s := NewStore()
// check to see if key exists
_, err := s.Get("Cory")
if err != nil {
if _, ok := err.(errNotFound); !ok {
log.Fatal(err)
}
s.Set("Cory", "cory@gopherguides.com")
}
v, err := s.Get("Cory")
if err != nil {
log.Fatal(err)
}
fmt.Println(v)
}
type Store struct {
keys map[string]string
}
func NewStore() *Store {
return &Store{
keys: make(map[string]string),
}
}
func (s *Store) Set(k, v string) {
s.keys[k] = v
}
func (s *Store) Get(k string) (string, error) {
if v, ok := s.keys[k]; ok {
return v, nil
}
return "", errNotFound{key: k}
}package main
import (
"fmt"
"log"
)
type errNotFound struct {
key string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("couldn't find value for key: %s", e.key)
}
func main() {
s := NewStore()
// check to see if key exists
_, err := s.Get("Cory")
if err != nil {
if _, ok := err.(errNotFound); !ok {
log.Fatal(err)
}
s.Set("Cory", "cory@gopherguides.com")
}
v, err := s.Get("Cory")
if err != nil {
log.Fatal(err)
}
fmt.Println(v)
}
type Store struct {
keys map[string]string
}
func NewStore() *Store {
return &Store{
keys: make(map[string]string),
}
}
func (s *Store) Set(k, v string) {
s.keys[k] = v
}
func (s *Store) Get(k string) (string, error) {
if v, ok := s.keys[k]; ok {
return v, nil
}
return "", errNotFound{key: k}
}Wrapping Errors
It is very common when writing code to wrap your error with more context. You can wrap them with the %w verb when using the fmt.Errorf function. This will allow for the use of several helpers later which we will cover.
In the following code, we are trying to open a config file. If we can’t find it, we return an error. However, since we want to be as specific as possible, we wrap the error in an additional statement so it makes our code easier to debug at a later time.
func main() {
c, err := openConfig("./config.json")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
f, err := os.Open(path)
if err != nil {
// Wrap the error in our own message for the future
return nil, fmt.Errorf("couldn't open config file: %w", err)
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
// section: code
func main() {
c, err := openConfig("./config.json")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
f, err := os.Open(path)
if err != nil {
// Wrap the error in our own message for the future
return nil, fmt.Errorf("couldn't open config file: %w", err)
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
// section: code
/*
// section: output
2020/04/18 13:00:39 couldn't open config file: open ./config.json: no such file or directory
// section: output
*/Output:
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
// section: code
func main() {
c, err := openConfig("./config.json")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
f, err := os.Open(path)
if err != nil {
// Wrap the error in our own message for the future
return nil, fmt.Errorf("couldn't open config file: %w", err)
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
// section: code
/*
// section: output
2020/04/18 13:00:39 couldn't open config file: open ./config.json: no such file or directory
// section: output
*/When To Wrap?
When returning errors, you need to decide if you want to wrap them or not. While normally you do, there may be scenarios that you do not.
- Wrap errors when you want to expose it to callers
- Don’t wrap when doing so may expose implementation details
You can read more about wrapping errors in this Go blog post.
Standard Library Adoption
The standard library increasingly uses error wrapping to provide better context. For example, as of Go 1.23, net.DNSError wraps errors caused by timeouts or cancellation, enabling patterns like:
_, err := net.LookupHost("example.com")
if errors.Is(err, context.DeadlineExceeded) {
// Handle timeout
}
if errors.Is(err, context.Canceled) {
// Handle cancellation
}
This demonstrates that error wrapping with %w isn’t just a best practice—it’s a pattern the Go team continues to adopt throughout the standard library.
Unwrapping Errors
Go 1.13 introduced several ways to unwrap an error to check for a specific error, and Go 1.26 added a generic version of errors.As.
- errors.AsType (added in Go 1.26)
- errors.As
- errors.Is
- errors.Unwrap
Errors.Is
You can use errors.Is to check for a sentinel error.
func main() {
c, err := openConfig("./config.json")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
var f io.Reader
var err error
f, err = os.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// use default config
f = bytes.NewBufferString(`{"backup":false,"debug":true}`)
} else {
return nil, fmt.Errorf("something went terribly wrong...: %w", err)
}
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
)
// section: code
func main() {
c, err := openConfig("./config.json")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
var f io.Reader
var err error
f, err = os.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// use default config
f = bytes.NewBufferString(`{"backup":false,"debug":true}`)
} else {
return nil, fmt.Errorf("something went terribly wrong...: %w", err)
}
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
// section: code
/*
// section: output
&{Backup:false Debug:true}
// section: output
*/Output:
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
)
// section: code
func main() {
c, err := openConfig("./config.json")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", c)
}
type config struct {
Backup bool `json:"backup"`
Debug bool `json:"debug"`
}
func openConfig(path string) (*config, error) {
var f io.Reader
var err error
f, err = os.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// use default config
f = bytes.NewBufferString(`{"backup":false,"debug":true}`)
} else {
return nil, fmt.Errorf("something went terribly wrong...: %w", err)
}
}
c := config{}
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, err
}
return &c, nil
}
// section: code
/*
// section: output
&{Backup:false Debug:true}
// section: output
*/Errors.AsType
To check for a specific error type, use the generic errors.AsType function, added in Go 1.26. You pass the type you are looking for as a type argument, and it returns the matched error along with a bool:
func main() {
err := boom()
if err != nil {
log.Println("error: ", err)
}
ce, ok := errors.AsType[ComplexError](err)
if ok {
log.Println("server:", ce.Server)
}
}
package main
import (
"errors"
"fmt"
"log"
)
// section: error
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func boom() error {
return ComplexError{
File: "main.go",
Server: "myhost",
}
}
// section: error
// section: main
func main() {
err := boom()
if err != nil {
log.Println("error: ", err)
}
ce, ok := errors.AsType[ComplexError](err)
if ok {
log.Println("server:", ce.Server)
}
}
// section: mainNotice that when ok is true, ce is already the concrete ComplexError, so we can access its fields directly.
Before Go 1.26, you used the two step errors.As function instead. You declare a variable of the target type first, then pass a pointer to it:
var ce ComplexError
if errors.As(err, &ce) {
log.Println("server:", ce.Server)
}
This form still works, and you’ll run across it in nearly every existing Go codebase, so you need to be able to read both.
Stacked Errors
Even if there are several errors stacked in a set of wrapped errors:
func boom() error {
ce := ComplexError{
File: "main.go",
Server: "myhost",
}
err := fmt.Errorf("original error: %w", ce)
err = fmt.Errorf("wrap again: %w", err)
return err
}
package main
import (
"errors"
"fmt"
"log"
)
// section: error
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
// section: wrap
func boom() error {
ce := ComplexError{
File: "main.go",
Server: "myhost",
}
err := fmt.Errorf("original error: %w", ce)
err = fmt.Errorf("wrap again: %w", err)
return err
}
// section: wrap
// section: error
// section: main
func main() {
err := boom()
if err != nil {
log.Println("error: ", err)
}
ce, ok := errors.AsType[ComplexError](err)
if ok {
log.Println("server:", ce.Server)
}
}
// section: main
/*
// section: output
2026/07/10 15:09:07 error: wrap again: original error: main.go: myhost
2026/07/10 15:09:07 server: myhost
// section: output
*/errors.AsType will traverse the stack until it finds the first instance of the error that matches the type you asked for (errors.As walks the stack the same way):
func main() {
err := boom()
if err != nil {
log.Println("error: ", err)
}
ce, ok := errors.AsType[ComplexError](err)
if ok {
log.Println("server:", ce.Server)
}
}
package main
import (
"errors"
"fmt"
"log"
)
// section: error
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
// section: wrap
func boom() error {
ce := ComplexError{
File: "main.go",
Server: "myhost",
}
err := fmt.Errorf("original error: %w", ce)
err = fmt.Errorf("wrap again: %w", err)
return err
}
// section: wrap
// section: error
// section: main
func main() {
err := boom()
if err != nil {
log.Println("error: ", err)
}
ce, ok := errors.AsType[ComplexError](err)
if ok {
log.Println("server:", ce.Server)
}
}
// section: main
/*
// section: output
2026/07/10 15:09:07 error: wrap again: original error: main.go: myhost
2026/07/10 15:09:07 server: myhost
// section: output
*/Output:
2026/07/10 15:09:07 error: wrap again: original error: main.go: myhost
2026/07/10 15:09:07 server: myhostpackage main
import (
"errors"
"fmt"
"log"
)
// section: error
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
// section: wrap
func boom() error {
ce := ComplexError{
File: "main.go",
Server: "myhost",
}
err := fmt.Errorf("original error: %w", ce)
err = fmt.Errorf("wrap again: %w", err)
return err
}
// section: wrap
// section: error
// section: main
func main() {
err := boom()
if err != nil {
log.Println("error: ", err)
}
ce, ok := errors.AsType[ComplexError](err)
if ok {
log.Println("server:", ce.Server)
}
}
// section: main
/*
// section: output
2026/07/10 15:09:07 error: wrap again: original error: main.go: myhost
2026/07/10 15:09:07 server: myhost
// section: output
*/Declaring Custom Unwrap Behavior
You can also implement Unwrap in your own errors as well.
type ComplexError struct {
File string
Server string
err error
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s - %s", c.File, c.Server, c.err)
}
func (c ComplexError) Unwrap() error {
return c.err
}
func boom() error {
return ComplexError{
File: "main.go",
Server: "myhost",
err: io.EOF,
}
}
func main() {
err := boom()
if e := errors.Unwrap(err); err != nil {
fmt.Printf("original error was: %s", e)
}
}
package main
import (
"fmt"
"io"
"errors"
)
// section: code
type ComplexError struct {
File string
Server string
err error
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s - %s", c.File, c.Server, c.err)
}
func (c ComplexError) Unwrap() error {
return c.err
}
func boom() error {
return ComplexError{
File: "main.go",
Server: "myhost",
err: io.EOF,
}
}
func main() {
err := boom()
if e := errors.Unwrap(err); err != nil {
fmt.Printf("original error was: %s", e)
}
}
// section: code
/*
// section: output
original error was: EOF
// section: output
*/Output:
package main
import (
"fmt"
"io"
"errors"
)
// section: code
type ComplexError struct {
File string
Server string
err error
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s - %s", c.File, c.Server, c.err)
}
func (c ComplexError) Unwrap() error {
return c.err
}
func boom() error {
return ComplexError{
File: "main.go",
Server: "myhost",
err: io.EOF,
}
}
func main() {
err := boom()
if e := errors.Unwrap(err); err != nil {
fmt.Printf("original error was: %s", e)
}
}
// section: code
/*
// section: output
original error was: EOF
// section: output
*/Errors.Unwrap
Any time you wrap an error by using the %w verb in fmt.Errorf you are able to Unwrap it later.
You can unwrap until you get to nil which means you are likely at the beginning of where the errors started.
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func boom() error {
ce := ComplexError{
File: "main.go",
Server: "myhost",
}
err := fmt.Errorf("original error: %w", ce)
err = fmt.Errorf("wrap again: %w", err)
return err
}
func main() {
err := boom()
// Unwrap and print all errors
for {
if err == nil {
return
}
fmt.Println(err)
err = errors.Unwrap(err)
}
}
package main
import (
"errors"
"fmt"
)
// section: code
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func boom() error {
ce := ComplexError{
File: "main.go",
Server: "myhost",
}
err := fmt.Errorf("original error: %w", ce)
err = fmt.Errorf("wrap again: %w", err)
return err
}
func main() {
err := boom()
// Unwrap and print all errors
for {
if err == nil {
return
}
fmt.Println(err)
err = errors.Unwrap(err)
}
}
// section: code
/*
// section: output
wrap again: original error: main.go: myhost
original error: main.go: myhost
main.go: myhost
// section: output
*/Output:
package main
import (
"errors"
"fmt"
)
// section: code
type ComplexError struct {
File string
Server string
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func boom() error {
ce := ComplexError{
File: "main.go",
Server: "myhost",
}
err := fmt.Errorf("original error: %w", ce)
err = fmt.Errorf("wrap again: %w", err)
return err
}
func main() {
err := boom()
// Unwrap and print all errors
for {
if err == nil {
return
}
fmt.Println(err)
err = errors.Unwrap(err)
}
}
// section: code
/*
// section: output
wrap again: original error: main.go: myhost
original error: main.go: myhost
main.go: myhost
// section: output
*/Unwrapping Multiple Errors
Because errors.Is and errors.AsType will walk the stack of errors, it is safe to use either of those functions on the same error. The older errors.As walks the stack the same way:
var errUnknown = errors.New("unknown error")
type ComplexError struct {
File string
Server string
err error
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func (c ComplexError) Unwrap() error {
return c.err
}
func main() {
err := boom()
if errors.Is(err, errUnknown) {
fmt.Println("found simple error")
}
if _, ok := errors.AsType[ComplexError](err); ok {
fmt.Println("found complex error")
}
}
func boom() error {
ce := ComplexError{
File: "README.md",
Server: "localhost:9090",
err: errUnknown,
}
return fmt.Errorf("wrapping a complex error: %w", ce)
}
package main
import (
"errors"
"fmt"
)
// section: main
var errUnknown = errors.New("unknown error")
type ComplexError struct {
File string
Server string
err error
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func (c ComplexError) Unwrap() error {
return c.err
}
func main() {
err := boom()
if errors.Is(err, errUnknown) {
fmt.Println("found simple error")
}
if _, ok := errors.AsType[ComplexError](err); ok {
fmt.Println("found complex error")
}
}
func boom() error {
ce := ComplexError{
File: "README.md",
Server: "localhost:9090",
err: errUnknown,
}
return fmt.Errorf("wrapping a complex error: %w", ce)
}
// section: main
/*
// section: output
found simple error
found complex error
// section: output
*/Output:
package main
import (
"errors"
"fmt"
)
// section: main
var errUnknown = errors.New("unknown error")
type ComplexError struct {
File string
Server string
err error
}
func (c ComplexError) Error() string {
return fmt.Sprintf("%s: %s", c.File, c.Server)
}
func (c ComplexError) Unwrap() error {
return c.err
}
func main() {
err := boom()
if errors.Is(err, errUnknown) {
fmt.Println("found simple error")
}
if _, ok := errors.AsType[ComplexError](err); ok {
fmt.Println("found complex error")
}
}
func boom() error {
ce := ComplexError{
File: "README.md",
Server: "localhost:9090",
err: errUnknown,
}
return fmt.Errorf("wrapping a complex error: %w", ce)
}
// section: main
/*
// section: output
found simple error
found complex error
// section: output
*/Advanced Error Chain Traversal
For complex applications, you may need to traverse and inspect entire error chains. This is useful for debugging, logging, and understanding the full context of an error.
// Advanced error chain traversal patterns demonstrate
// how to inspect and navigate complex error chains
type ValidationError struct {
Field string
Message string
err error
}
func (v *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", v.Field, v.Message)
}
func (v *ValidationError) Unwrap() error {
return v.err
}
type DatabaseError struct {
Query string
err error
}
func (d *DatabaseError) Error() string {
return fmt.Sprintf("database error executing query: %s", d.Query)
}
func (d *DatabaseError) Unwrap() error {
return d.err
}
// collectErrorChain traverses the entire error chain and returns all errors
func collectErrorChain(err error) []error {
var chain []error
for err != nil {
chain = append(chain, err)
err = errors.Unwrap(err)
}
return chain
}
// findErrorInChain searches for a specific error type in the chain
func findErrorInChain[T error](err error) (T, bool) {
for err != nil {
if target, ok := errors.AsType[T](err); ok {
return target, true
}
err = errors.Unwrap(err)
}
var zero T
return zero, false
}
// printErrorChain displays the entire error chain with depth indicators
func printErrorChain(err error) {
chain := collectErrorChain(err)
fmt.Println("Error Chain:")
for i, e := range chain {
indent := ""
for j := 0; j < i; j++ {
indent += " "
}
fmt.Printf("%s[%d] %v\n", indent, i, e)
}
}
func simulateError() error {
// Start with a base error
baseErr := io.ErrUnexpectedEOF
// Wrap in a database error
dbErr := &DatabaseError{
Query: "SELECT * FROM users WHERE id = ?",
err: baseErr,
}
// Wrap in a validation error
valErr := &ValidationError{
Field: "user_id",
Message: "failed to fetch user",
err: dbErr,
}
// Wrap with additional context
return fmt.Errorf("operation failed: %w", valErr)
}
func main() {
err := simulateError()
// Print the entire error chain
printErrorChain(err)
fmt.Println("\n--- Error Inspection ---")
// Check for specific error in the chain
if errors.Is(err, io.ErrUnexpectedEOF) {
fmt.Println("✓ Chain contains io.ErrUnexpectedEOF")
}
// Extract specific error type from chain
if dbErr, ok := errors.AsType[*DatabaseError](err); ok {
fmt.Printf("✓ Found DatabaseError with query: %s\n", dbErr.Query)
}
if valErr, ok := errors.AsType[*ValidationError](err); ok {
fmt.Printf("✓ Found ValidationError on field: %s\n", valErr.Field)
}
fmt.Println("\n--- Manual Chain Traversal ---")
// Manually walk the chain
currentErr := err
depth := 0
for currentErr != nil {
fmt.Printf("Level %d: %T\n", depth, currentErr)
currentErr = errors.Unwrap(currentErr)
depth++
}
}
package main
import (
"errors"
"fmt"
"io"
)
// section: code
// Advanced error chain traversal patterns demonstrate
// how to inspect and navigate complex error chains
type ValidationError struct {
Field string
Message string
err error
}
func (v *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", v.Field, v.Message)
}
func (v *ValidationError) Unwrap() error {
return v.err
}
type DatabaseError struct {
Query string
err error
}
func (d *DatabaseError) Error() string {
return fmt.Sprintf("database error executing query: %s", d.Query)
}
func (d *DatabaseError) Unwrap() error {
return d.err
}
// collectErrorChain traverses the entire error chain and returns all errors
func collectErrorChain(err error) []error {
var chain []error
for err != nil {
chain = append(chain, err)
err = errors.Unwrap(err)
}
return chain
}
// findErrorInChain searches for a specific error type in the chain
func findErrorInChain[T error](err error) (T, bool) {
for err != nil {
if target, ok := errors.AsType[T](err); ok {
return target, true
}
err = errors.Unwrap(err)
}
var zero T
return zero, false
}
// printErrorChain displays the entire error chain with depth indicators
func printErrorChain(err error) {
chain := collectErrorChain(err)
fmt.Println("Error Chain:")
for i, e := range chain {
indent := ""
for j := 0; j < i; j++ {
indent += " "
}
fmt.Printf("%s[%d] %v\n", indent, i, e)
}
}
func simulateError() error {
// Start with a base error
baseErr := io.ErrUnexpectedEOF
// Wrap in a database error
dbErr := &DatabaseError{
Query: "SELECT * FROM users WHERE id = ?",
err: baseErr,
}
// Wrap in a validation error
valErr := &ValidationError{
Field: "user_id",
Message: "failed to fetch user",
err: dbErr,
}
// Wrap with additional context
return fmt.Errorf("operation failed: %w", valErr)
}
func main() {
err := simulateError()
// Print the entire error chain
printErrorChain(err)
fmt.Println("\n--- Error Inspection ---")
// Check for specific error in the chain
if errors.Is(err, io.ErrUnexpectedEOF) {
fmt.Println("✓ Chain contains io.ErrUnexpectedEOF")
}
// Extract specific error type from chain
if dbErr, ok := errors.AsType[*DatabaseError](err); ok {
fmt.Printf("✓ Found DatabaseError with query: %s\n", dbErr.Query)
}
if valErr, ok := errors.AsType[*ValidationError](err); ok {
fmt.Printf("✓ Found ValidationError on field: %s\n", valErr.Field)
}
fmt.Println("\n--- Manual Chain Traversal ---")
// Manually walk the chain
currentErr := err
depth := 0
for currentErr != nil {
fmt.Printf("Level %d: %T\n", depth, currentErr)
currentErr = errors.Unwrap(currentErr)
depth++
}
}
// section: code
/*
// section: output
Error Chain:
[0] operation failed: validation error on field user_id: failed to fetch user
[1] validation error on field user_id: failed to fetch user
[2] database error executing query: SELECT * FROM users WHERE id = ?
[3] unexpected EOF
--- Error Inspection ---
✓ Chain contains io.ErrUnexpectedEOF
✓ Found DatabaseError with query: SELECT * FROM users WHERE id = ?
✓ Found ValidationError on field: user_id
--- Manual Chain Traversal ---
Level 0: *fmt.wrapError
Level 1: *main.ValidationError
Level 2: *main.DatabaseError
Level 3: *errors.errorString
// section: output
*/Output:
Error Chain:
[0] operation failed: validation error on field user_id: failed to fetch user
[1] validation error on field user_id: failed to fetch user
[2] database error executing query: SELECT * FROM users WHERE id = ?
[3] unexpected EOF
--- Error Inspection ---
✓ Chain contains io.ErrUnexpectedEOF
✓ Found DatabaseError with query: SELECT * FROM users WHERE id = ?
✓ Found ValidationError on field: user_id
--- Manual Chain Traversal ---
Level 0: *fmt.wrapError
Level 1: *main.ValidationError
Level 2: *main.DatabaseError
Level 3: *errors.errorStringpackage main
import (
"errors"
"fmt"
"io"
)
// section: code
// Advanced error chain traversal patterns demonstrate
// how to inspect and navigate complex error chains
type ValidationError struct {
Field string
Message string
err error
}
func (v *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", v.Field, v.Message)
}
func (v *ValidationError) Unwrap() error {
return v.err
}
type DatabaseError struct {
Query string
err error
}
func (d *DatabaseError) Error() string {
return fmt.Sprintf("database error executing query: %s", d.Query)
}
func (d *DatabaseError) Unwrap() error {
return d.err
}
// collectErrorChain traverses the entire error chain and returns all errors
func collectErrorChain(err error) []error {
var chain []error
for err != nil {
chain = append(chain, err)
err = errors.Unwrap(err)
}
return chain
}
// findErrorInChain searches for a specific error type in the chain
func findErrorInChain[T error](err error) (T, bool) {
for err != nil {
if target, ok := errors.AsType[T](err); ok {
return target, true
}
err = errors.Unwrap(err)
}
var zero T
return zero, false
}
// printErrorChain displays the entire error chain with depth indicators
func printErrorChain(err error) {
chain := collectErrorChain(err)
fmt.Println("Error Chain:")
for i, e := range chain {
indent := ""
for j := 0; j < i; j++ {
indent += " "
}
fmt.Printf("%s[%d] %v\n", indent, i, e)
}
}
func simulateError() error {
// Start with a base error
baseErr := io.ErrUnexpectedEOF
// Wrap in a database error
dbErr := &DatabaseError{
Query: "SELECT * FROM users WHERE id = ?",
err: baseErr,
}
// Wrap in a validation error
valErr := &ValidationError{
Field: "user_id",
Message: "failed to fetch user",
err: dbErr,
}
// Wrap with additional context
return fmt.Errorf("operation failed: %w", valErr)
}
func main() {
err := simulateError()
// Print the entire error chain
printErrorChain(err)
fmt.Println("\n--- Error Inspection ---")
// Check for specific error in the chain
if errors.Is(err, io.ErrUnexpectedEOF) {
fmt.Println("✓ Chain contains io.ErrUnexpectedEOF")
}
// Extract specific error type from chain
if dbErr, ok := errors.AsType[*DatabaseError](err); ok {
fmt.Printf("✓ Found DatabaseError with query: %s\n", dbErr.Query)
}
if valErr, ok := errors.AsType[*ValidationError](err); ok {
fmt.Printf("✓ Found ValidationError on field: %s\n", valErr.Field)
}
fmt.Println("\n--- Manual Chain Traversal ---")
// Manually walk the chain
currentErr := err
depth := 0
for currentErr != nil {
fmt.Printf("Level %d: %T\n", depth, currentErr)
currentErr = errors.Unwrap(currentErr)
depth++
}
}
// section: code
/*
// section: output
Error Chain:
[0] operation failed: validation error on field user_id: failed to fetch user
[1] validation error on field user_id: failed to fetch user
[2] database error executing query: SELECT * FROM users WHERE id = ?
[3] unexpected EOF
--- Error Inspection ---
✓ Chain contains io.ErrUnexpectedEOF
✓ Found DatabaseError with query: SELECT * FROM users WHERE id = ?
✓ Found ValidationError on field: user_id
--- Manual Chain Traversal ---
Level 0: *fmt.wrapError
Level 1: *main.ValidationError
Level 2: *main.DatabaseError
Level 3: *errors.errorString
// section: output
*/Custom Unmarshal Error Handling
When working with JSON or other data formats, it’s important to provide clear error messages about what went wrong during unmarshaling. Custom error types can help provide detailed context about validation failures and type mismatches.
// Custom error handling for JSON unmarshaling with detailed error information
// UnmarshalError provides detailed information about JSON unmarshaling failures
type UnmarshalError struct {
Data string
FieldPath string
Reason string
err error
}
func (u *UnmarshalError) Error() string {
return fmt.Sprintf("unmarshal error at %s: %s", u.FieldPath, u.Reason)
}
func (u *UnmarshalError) Unwrap() error {
return u.err
}
// Config represents application configuration
type Config struct {
Port int `json:"port"`
Host string `json:"host"`
Features Features `json:"features"`
}
type Features struct {
Debug bool `json:"debug"`
Logging bool `json:"logging"`
}
// UnmarshalJSON implements custom unmarshaling with detailed error handling
func (c *Config) UnmarshalJSON(data []byte) error {
// Use an alias to avoid infinite recursion
type Alias Config
aux := &struct {
*Alias
}{
Alias: (*Alias)(c),
}
if err := json.Unmarshal(data, &aux); err != nil {
// Extract field information from JSON error
if syntaxErr, ok := errors.AsType[*json.SyntaxError](err); ok {
return &UnmarshalError{
Data: string(data),
FieldPath: fmt.Sprintf("offset %d", syntaxErr.Offset),
Reason: syntaxErr.Error(),
err: err,
}
}
if unmarshalErr, ok := errors.AsType[*json.UnmarshalTypeError](err); ok {
return &UnmarshalError{
Data: string(data),
FieldPath: unmarshalErr.Field,
Reason: fmt.Sprintf("expected %s but got %s", unmarshalErr.Type, unmarshalErr.Value),
err: err,
}
}
return &UnmarshalError{
Data: string(data),
FieldPath: "unknown",
Reason: err.Error(),
err: err,
}
}
// Validate configuration
if c.Port < 1 || c.Port > 65535 {
return &UnmarshalError{
Data: string(data),
FieldPath: "port",
Reason: fmt.Sprintf("port must be between 1 and 65535, got %d", c.Port),
err: errors.New("invalid port number"),
}
}
if c.Host == "" {
return &UnmarshalError{
Data: string(data),
FieldPath: "host",
Reason: "host cannot be empty",
err: errors.New("missing required field"),
}
}
return nil
}
// parseConfig attempts to parse JSON configuration with detailed error reporting
func parseConfig(jsonData string) (*Config, error) {
var config Config
err := json.Unmarshal([]byte(jsonData), &config)
if err != nil {
return nil, fmt.Errorf("failed to parse configuration: %w", err)
}
return &config, nil
}
// handleUnmarshalError demonstrates how to handle unmarshal errors
func handleUnmarshalError(err error) {
unmarshalErr, ok := errors.AsType[*UnmarshalError](err)
if !ok {
fmt.Printf("Unexpected error: %v\n", err)
return
}
fmt.Printf("Field: %s\n", unmarshalErr.FieldPath)
fmt.Printf("Reason: %s\n", unmarshalErr.Reason)
fmt.Printf("Data: %s\n", strings.TrimSpace(unmarshalErr.Data))
// Check for underlying JSON errors
if typeErr, ok := errors.AsType[*json.UnmarshalTypeError](err); ok {
fmt.Printf("Type mismatch: field=%s, expected=%s, got=%s\n",
typeErr.Field, typeErr.Type, typeErr.Value)
}
}
func main() {
// Example 1: Type mismatch error
fmt.Println("=== Example 1: Type Mismatch ===")
invalidTypeJSON := `{"port": "not-a-number", "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err := parseConfig(invalidTypeJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 2: Validation Error ===")
invalidPortJSON := `{"port": 99999, "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(invalidPortJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 3: Missing Required Field ===")
missingFieldJSON := `{"port": 8080, "host": "", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(missingFieldJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 4: Valid Configuration ===")
validJSON := `{"port": 8080, "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(validJSON)
if err != nil {
handleUnmarshalError(err)
} else {
fmt.Printf("✓ Configuration loaded successfully: %+v\n", config)
}
}
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// section: code
// Custom error handling for JSON unmarshaling with detailed error information
// UnmarshalError provides detailed information about JSON unmarshaling failures
type UnmarshalError struct {
Data string
FieldPath string
Reason string
err error
}
func (u *UnmarshalError) Error() string {
return fmt.Sprintf("unmarshal error at %s: %s", u.FieldPath, u.Reason)
}
func (u *UnmarshalError) Unwrap() error {
return u.err
}
// Config represents application configuration
type Config struct {
Port int `json:"port"`
Host string `json:"host"`
Features Features `json:"features"`
}
type Features struct {
Debug bool `json:"debug"`
Logging bool `json:"logging"`
}
// UnmarshalJSON implements custom unmarshaling with detailed error handling
func (c *Config) UnmarshalJSON(data []byte) error {
// Use an alias to avoid infinite recursion
type Alias Config
aux := &struct {
*Alias
}{
Alias: (*Alias)(c),
}
if err := json.Unmarshal(data, &aux); err != nil {
// Extract field information from JSON error
if syntaxErr, ok := errors.AsType[*json.SyntaxError](err); ok {
return &UnmarshalError{
Data: string(data),
FieldPath: fmt.Sprintf("offset %d", syntaxErr.Offset),
Reason: syntaxErr.Error(),
err: err,
}
}
if unmarshalErr, ok := errors.AsType[*json.UnmarshalTypeError](err); ok {
return &UnmarshalError{
Data: string(data),
FieldPath: unmarshalErr.Field,
Reason: fmt.Sprintf("expected %s but got %s", unmarshalErr.Type, unmarshalErr.Value),
err: err,
}
}
return &UnmarshalError{
Data: string(data),
FieldPath: "unknown",
Reason: err.Error(),
err: err,
}
}
// Validate configuration
if c.Port < 1 || c.Port > 65535 {
return &UnmarshalError{
Data: string(data),
FieldPath: "port",
Reason: fmt.Sprintf("port must be between 1 and 65535, got %d", c.Port),
err: errors.New("invalid port number"),
}
}
if c.Host == "" {
return &UnmarshalError{
Data: string(data),
FieldPath: "host",
Reason: "host cannot be empty",
err: errors.New("missing required field"),
}
}
return nil
}
// parseConfig attempts to parse JSON configuration with detailed error reporting
func parseConfig(jsonData string) (*Config, error) {
var config Config
err := json.Unmarshal([]byte(jsonData), &config)
if err != nil {
return nil, fmt.Errorf("failed to parse configuration: %w", err)
}
return &config, nil
}
// handleUnmarshalError demonstrates how to handle unmarshal errors
func handleUnmarshalError(err error) {
unmarshalErr, ok := errors.AsType[*UnmarshalError](err)
if !ok {
fmt.Printf("Unexpected error: %v\n", err)
return
}
fmt.Printf("Field: %s\n", unmarshalErr.FieldPath)
fmt.Printf("Reason: %s\n", unmarshalErr.Reason)
fmt.Printf("Data: %s\n", strings.TrimSpace(unmarshalErr.Data))
// Check for underlying JSON errors
if typeErr, ok := errors.AsType[*json.UnmarshalTypeError](err); ok {
fmt.Printf("Type mismatch: field=%s, expected=%s, got=%s\n",
typeErr.Field, typeErr.Type, typeErr.Value)
}
}
func main() {
// Example 1: Type mismatch error
fmt.Println("=== Example 1: Type Mismatch ===")
invalidTypeJSON := `{"port": "not-a-number", "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err := parseConfig(invalidTypeJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 2: Validation Error ===")
invalidPortJSON := `{"port": 99999, "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(invalidPortJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 3: Missing Required Field ===")
missingFieldJSON := `{"port": 8080, "host": "", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(missingFieldJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 4: Valid Configuration ===")
validJSON := `{"port": 8080, "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(validJSON)
if err != nil {
handleUnmarshalError(err)
} else {
fmt.Printf("✓ Configuration loaded successfully: %+v\n", config)
}
}
// section: code
/*
// section: output
=== Example 1: Type Mismatch ===
Field: Alias.port
Reason: expected int but got string
Data: {"port": "not-a-number", "host": "localhost", "features": {"debug": true, "logging": false}}
Type mismatch: field=Alias.port, expected=int, got=string
=== Example 2: Validation Error ===
Field: port
Reason: port must be between 1 and 65535, got 99999
Data: {"port": 99999, "host": "localhost", "features": {"debug": true, "logging": false}}
=== Example 3: Missing Required Field ===
Field: host
Reason: host cannot be empty
Data: {"port": 8080, "host": "", "features": {"debug": true, "logging": false}}
=== Example 4: Valid Configuration ===
✓ Configuration loaded successfully: &{Port:8080 Host:localhost Features:{Debug:true Logging:false}}
// section: output
*/Output:
=== Example 1: Type Mismatch ===
Field: Alias.port
Reason: expected int but got string
Data: {"port": "not-a-number", "host": "localhost", "features": {"debug": true, "logging": false}}
Type mismatch: field=Alias.port, expected=int, got=string
=== Example 2: Validation Error ===
Field: port
Reason: port must be between 1 and 65535, got 99999
Data: {"port": 99999, "host": "localhost", "features": {"debug": true, "logging": false}}
=== Example 3: Missing Required Field ===
Field: host
Reason: host cannot be empty
Data: {"port": 8080, "host": "", "features": {"debug": true, "logging": false}}
=== Example 4: Valid Configuration ===
✓ Configuration loaded successfully: &{Port:8080 Host:localhost Features:{Debug:true Logging:false}}package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// section: code
// Custom error handling for JSON unmarshaling with detailed error information
// UnmarshalError provides detailed information about JSON unmarshaling failures
type UnmarshalError struct {
Data string
FieldPath string
Reason string
err error
}
func (u *UnmarshalError) Error() string {
return fmt.Sprintf("unmarshal error at %s: %s", u.FieldPath, u.Reason)
}
func (u *UnmarshalError) Unwrap() error {
return u.err
}
// Config represents application configuration
type Config struct {
Port int `json:"port"`
Host string `json:"host"`
Features Features `json:"features"`
}
type Features struct {
Debug bool `json:"debug"`
Logging bool `json:"logging"`
}
// UnmarshalJSON implements custom unmarshaling with detailed error handling
func (c *Config) UnmarshalJSON(data []byte) error {
// Use an alias to avoid infinite recursion
type Alias Config
aux := &struct {
*Alias
}{
Alias: (*Alias)(c),
}
if err := json.Unmarshal(data, &aux); err != nil {
// Extract field information from JSON error
if syntaxErr, ok := errors.AsType[*json.SyntaxError](err); ok {
return &UnmarshalError{
Data: string(data),
FieldPath: fmt.Sprintf("offset %d", syntaxErr.Offset),
Reason: syntaxErr.Error(),
err: err,
}
}
if unmarshalErr, ok := errors.AsType[*json.UnmarshalTypeError](err); ok {
return &UnmarshalError{
Data: string(data),
FieldPath: unmarshalErr.Field,
Reason: fmt.Sprintf("expected %s but got %s", unmarshalErr.Type, unmarshalErr.Value),
err: err,
}
}
return &UnmarshalError{
Data: string(data),
FieldPath: "unknown",
Reason: err.Error(),
err: err,
}
}
// Validate configuration
if c.Port < 1 || c.Port > 65535 {
return &UnmarshalError{
Data: string(data),
FieldPath: "port",
Reason: fmt.Sprintf("port must be between 1 and 65535, got %d", c.Port),
err: errors.New("invalid port number"),
}
}
if c.Host == "" {
return &UnmarshalError{
Data: string(data),
FieldPath: "host",
Reason: "host cannot be empty",
err: errors.New("missing required field"),
}
}
return nil
}
// parseConfig attempts to parse JSON configuration with detailed error reporting
func parseConfig(jsonData string) (*Config, error) {
var config Config
err := json.Unmarshal([]byte(jsonData), &config)
if err != nil {
return nil, fmt.Errorf("failed to parse configuration: %w", err)
}
return &config, nil
}
// handleUnmarshalError demonstrates how to handle unmarshal errors
func handleUnmarshalError(err error) {
unmarshalErr, ok := errors.AsType[*UnmarshalError](err)
if !ok {
fmt.Printf("Unexpected error: %v\n", err)
return
}
fmt.Printf("Field: %s\n", unmarshalErr.FieldPath)
fmt.Printf("Reason: %s\n", unmarshalErr.Reason)
fmt.Printf("Data: %s\n", strings.TrimSpace(unmarshalErr.Data))
// Check for underlying JSON errors
if typeErr, ok := errors.AsType[*json.UnmarshalTypeError](err); ok {
fmt.Printf("Type mismatch: field=%s, expected=%s, got=%s\n",
typeErr.Field, typeErr.Type, typeErr.Value)
}
}
func main() {
// Example 1: Type mismatch error
fmt.Println("=== Example 1: Type Mismatch ===")
invalidTypeJSON := `{"port": "not-a-number", "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err := parseConfig(invalidTypeJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 2: Validation Error ===")
invalidPortJSON := `{"port": 99999, "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(invalidPortJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 3: Missing Required Field ===")
missingFieldJSON := `{"port": 8080, "host": "", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(missingFieldJSON)
if err != nil {
handleUnmarshalError(err)
}
fmt.Println("\n=== Example 4: Valid Configuration ===")
validJSON := `{"port": 8080, "host": "localhost", "features": {"debug": true, "logging": false}}`
config, err = parseConfig(validJSON)
if err != nil {
handleUnmarshalError(err)
} else {
fmt.Printf("✓ Configuration loaded successfully: %+v\n", config)
}
}
// section: code
/*
// section: output
=== Example 1: Type Mismatch ===
Field: Alias.port
Reason: expected int but got string
Data: {"port": "not-a-number", "host": "localhost", "features": {"debug": true, "logging": false}}
Type mismatch: field=Alias.port, expected=int, got=string
=== Example 2: Validation Error ===
Field: port
Reason: port must be between 1 and 65535, got 99999
Data: {"port": 99999, "host": "localhost", "features": {"debug": true, "logging": false}}
=== Example 3: Missing Required Field ===
Field: host
Reason: host cannot be empty
Data: {"port": 8080, "host": "", "features": {"debug": true, "logging": false}}
=== Example 4: Valid Configuration ===
✓ Configuration loaded successfully: &{Port:8080 Host:localhost Features:{Debug:true Logging:false}}
// section: output
*/Nil Typed Errors
Be wary of comparing nil types. Even though you return nil from a function, if it has a type, it will not be nil in the same way that an error is nil.
The reason for this is that Go stores the information of an interface as both the type and the value. An interface value is only truly nil if both the type and the value are not set.
In this case, the interface is {Type: *errFound, Value: nil} and not {Type: nil, Value: nil}
type errFound string
func (e *errFound) Error() string {
return "found"
}
func main() {
err := exists("README.md")
if err != nil {
fmt.Println("error was NOT nil")
if _, ok := errors.AsType[*errFound](err); ok {
fmt.Println("this was a errFound...")
}
}
}
func exists(path string) error {
if path == "README.md" {
// return a `typed` nil value...
var e *errFound
if e == nil {
fmt.Println("yup, it's nil here..")
}
return e
}
return nil
}
package main
import (
"errors"
"fmt"
)
// section: main
type errFound string
func (e *errFound) Error() string {
return "found"
}
func main() {
err := exists("README.md")
if err != nil {
fmt.Println("error was NOT nil")
if _, ok := errors.AsType[*errFound](err); ok {
fmt.Println("this was a errFound...")
}
}
}
func exists(path string) error {
if path == "README.md" {
// return a `typed` nil value...
var e *errFound
if e == nil {
fmt.Println("yup, it's nil here..")
}
return e
}
return nil
}
// section: main
/*
// section: output
yup, it's nil here..
error was NOT nil
this was a errFound...
// section: output
*/Output:
package main
import (
"errors"
"fmt"
)
// section: main
type errFound string
func (e *errFound) Error() string {
return "found"
}
func main() {
err := exists("README.md")
if err != nil {
fmt.Println("error was NOT nil")
if _, ok := errors.AsType[*errFound](err); ok {
fmt.Println("this was a errFound...")
}
}
}
func exists(path string) error {
if path == "README.md" {
// return a `typed` nil value...
var e *errFound
if e == nil {
fmt.Println("yup, it's nil here..")
}
return e
}
return nil
}
// section: main
/*
// section: output
yup, it's nil here..
error was NOT nil
this was a errFound...
// section: output
*/Spot The Bug
The following code has a subtle yet common bug:
func main() {
// Check for the sentinel error of `io.EOF`
err := cat("./README.md")
if !errors.Is(err, io.EOF) {
log.Fatalf("something went unexpected: %s", err)
}
}
func cat(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
buff := make([]byte, 10)
for {
i, err := f.Read(buff)
if err != nil {
return fmt.Errorf("got a read error: %s", err)
}
if _, err := os.Stdout.Write(buff[:i]); err != nil {
return fmt.Errorf("write error: %s", err)
}
}
}
package main
import (
"errors"
"fmt"
"io"
"log"
"os"
)
// section: code
func main() {
// Check for the sentinel error of `io.EOF`
err := cat("./README.md")
if !errors.Is(err, io.EOF) {
log.Fatalf("something went unexpected: %s", err)
}
}
func cat(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
buff := make([]byte, 10)
for {
i, err := f.Read(buff)
if err != nil {
return fmt.Errorf("got a read error: %s", err)
}
if _, err := os.Stdout.Write(buff[:i]); err != nil {
return fmt.Errorf("write error: %s", err)
}
}
}
// section: code
/*
// section: output
# Title
Description of project
## Getting Started
Quick start guides here...
### Prerequisites
Here is what you need to know before you start...
2020/04/18 15:13:56 something went unexpected: got a read error: EOF
exit status 1
// section: output
*/Output:
# Title
Description of project
## Getting Started
Quick start guides here...
### Prerequisites
Here is what you need to know before you start...
2020/04/18 15:13:56 something went unexpected: got a read error: EOF
exit status 1package main
import (
"errors"
"fmt"
"io"
"log"
"os"
)
// section: code
func main() {
// Check for the sentinel error of `io.EOF`
err := cat("./README.md")
if !errors.Is(err, io.EOF) {
log.Fatalf("something went unexpected: %s", err)
}
}
func cat(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
buff := make([]byte, 10)
for {
i, err := f.Read(buff)
if err != nil {
return fmt.Errorf("got a read error: %s", err)
}
if _, err := os.Stdout.Write(buff[:i]); err != nil {
return fmt.Errorf("write error: %s", err)
}
}
}
// section: code
/*
// section: output
# Title
Description of project
## Getting Started
Quick start guides here...
### Prerequisites
Here is what you need to know before you start...
2020/04/18 15:13:56 something went unexpected: got a read error: EOF
exit status 1
// section: output
*/Bug Solution
Make sure to always wrap errors with the %w verb, and not the %s verb.
func main() {
// Check for the sentinel error of `io.EOF`
err := cat("./README.md")
if !errors.Is(err, io.EOF) {
log.Fatalf("something went unexpected: %s", err)
}
}
func cat(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
buff := make([]byte, 10)
for {
i, err := f.Read(buff)
if err != nil {
return fmt.Errorf("got a read error: %w", err)
}
if _, err := os.Stdout.Write(buff[:i]); err != nil {
return fmt.Errorf("write error: %w", err)
}
}
}
package main
import (
"errors"
"fmt"
"io"
"log"
"os"
)
// section: code
func main() {
// Check for the sentinel error of `io.EOF`
err := cat("./README.md")
if !errors.Is(err, io.EOF) {
log.Fatalf("something went unexpected: %s", err)
}
}
func cat(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
buff := make([]byte, 10)
for {
i, err := f.Read(buff)
if err != nil {
return fmt.Errorf("got a read error: %w", err)
}
if _, err := os.Stdout.Write(buff[:i]); err != nil {
return fmt.Errorf("write error: %w", err)
}
}
}
// section: code
/*
// section: output
# Title
Description of project
## Getting Started
Quick start guides here...
### Prerequisites
Here is what you need to know before you start...
// section: output
*/Output:
# Title
Description of project
## Getting Started
Quick start guides here...
### Prerequisites
Here is what you need to know before you start...package main
import (
"errors"
"fmt"
"io"
"log"
"os"
)
// section: code
func main() {
// Check for the sentinel error of `io.EOF`
err := cat("./README.md")
if !errors.Is(err, io.EOF) {
log.Fatalf("something went unexpected: %s", err)
}
}
func cat(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
buff := make([]byte, 10)
for {
i, err := f.Read(buff)
if err != nil {
return fmt.Errorf("got a read error: %w", err)
}
if _, err := os.Stdout.Write(buff[:i]); err != nil {
return fmt.Errorf("write error: %w", err)
}
}
}
// section: code
/*
// section: output
# Title
Description of project
## Getting Started
Quick start guides here...
### Prerequisites
Here is what you need to know before you start...
// section: output
*/Panics
Occasionally in your code you will do something that the Go runtime does not like.
a := []string{}
a[42] = "Bring a towel"
This code will panic and your application will crash.
panic: runtime error: index out of range
goroutine 1 [running]:
main.main()
panic.go:5 +0x11
Recover From A Panic
With a combination of the defer keyword and the the recover function we can recover from panics in our applications and gracefully handle them.
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("oh no, a panic occurred:", err)
}
}()
a := []string{}
a[42] = "Bring a towel"
}Using Recover
One of the most common reasons to use recover is when you allow the developer to provide a function as an argument to your function or method. Because it’s possible that the developer wrote bad code that could panic, it’s best to try to recover from that situation and report an error to the end user that they encountered a panic.
Looking at the following code, without using the recover pattern, we allow the vulnerability of the developer code panicing and thus shutting down our program:
// You can edit this code!
// Click here and start typing.
package main
import (
"fmt"
"log"
)
type matcher func(rune) bool
func main() {
m := func(r rune) bool {
defer panic("hahaha")
if r == 'g' || r == 'o' {
return true
}
// bad code....
return false
}
s, err := sanitize(m, "go is awesome")
if err != nil {
log.Fatal(err)
}
fmt.Println(s)
}
func sanitize(m matcher, s string) (string, error) {
var val string
for _, c := range s {
if m(c) {
val = val + "*"
continue
}
val = val + string(c)
}
return val, nil
}// You can edit this code!
// Click here and start typing.
package main
import (
"fmt"
"log"
)
type matcher func(rune) bool
func main() {
m := func(r rune) bool {
defer panic("hahaha")
if r == 'g' || r == 'o' {
return true
}
// bad code....
return false
}
s, err := sanitize(m, "go is awesome")
if err != nil {
log.Fatal(err)
}
fmt.Println(s)
}
func sanitize(m matcher, s string) (string, error) {
var val string
for _, c := range s {
if m(c) {
val = val + "*"
continue
}
val = val + string(c)
}
return val, nil
}The Fix With Recover
We can update the previous code with a defer/recover so that if the provided function panics, we can gracefully recover and send an error back that describes the error. This is very similar to what the net/http package does for handlers.
// You can edit this code!
// Click here and start typing.
package main
import (
"fmt"
"log"
)
type matcher func(rune) bool
func main() {
m := func(r rune) bool {
defer panic("hahaha")
if r == 'g' || r == 'o' {
return true
}
return false
}
s, err := sanitize(m, "go is awesome")
if err != nil {
log.Fatal(err)
}
fmt.Println(s)
}
func sanitize(m matcher, s string) (val string, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("invalid matcher. panic occurred: %v", e)
}
}()
for _, c := range s {
if m(c) {
val = val + "*"
continue
}
val = val + string(c)
}
return
}// You can edit this code!
// Click here and start typing.
package main
import (
"fmt"
"log"
)
type matcher func(rune) bool
func main() {
m := func(r rune) bool {
defer panic("hahaha")
if r == 'g' || r == 'o' {
return true
}
return false
}
s, err := sanitize(m, "go is awesome")
if err != nil {
log.Fatal(err)
}
fmt.Println(s)
}
func sanitize(m matcher, s string) (val string, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("invalid matcher. panic occurred: %v", e)
}
}()
for _, c := range s {
if m(c) {
val = val + "*"
continue
}
val = val + string(c)
}
return
}Avoid Panicking
In general, it is good practice not to panic. It is better to return an error and let others figure out how to handle it in their applications.
Alternate Errors Package
Previous to Go 1.13, there wasn’t a way to wrap errors. As such, a third-party package become very popular in the Go community. You’ll almost certainly run across this package at some point.
While the concepts in this package are close to the standard library, they don’t share the same API. As such, if you are trying to unwrap errors from third party packages as well as the standard library, you’ll have to check for both interfaces. You can do this manually, or you can use the errx package to solve this problem if needed.
Stack Traces
There may be times that you want to output a stack trace to a log, file, etc. You can do this by using the runtime/debug package.
package main
import "runtime/debug"
func main() {
First("Hello")
}
func First(s string) {
Second(s)
}
func Second(s string) {
Third(s)
}
func Third(s string) {
debug.PrintStack()
}package main
import "runtime/debug"
func main() {
First("Hello")
}
func First(s string) {
Second(s)
}
func Second(s string) {
Third(s)
}
func Third(s string) {
debug.PrintStack()
}Output:
goroutine 1 [running]:
runtime/debug.Stack(0xc000040778, 0xc000108f78, 0x1004685)
/usr/local/Cellar/go/1.16.6/libexec/src/runtime/debug/stack.go:24 +0x9f
runtime/debug.PrintStack()
/usr/local/Cellar/go/1.16.6/libexec/src/runtime/debug/stack.go:16 +0x25
main.Third(...)
/Users/corylanou/projects/gopherguides/learn/_training/fundamentals/errors/src/examples/stack/main.go:18
main.Second(...)
/Users/corylanou/projects/gopherguides/learn/_training/fundamentals/errors/src/examples/stack/main.go:14
main.First(...)
/Users/corylanou/projects/gopherguides/learn/_training/fundamentals/errors/src/examples/stack/main.go:10
main.main()
/Users/corylanou/projects/gopherguides/learn/_training/fundamentals/errors/src/examples/stack/main.go:6 +0x25goroutine 1 [running]:
runtime/debug.Stack(0xc000040778, 0xc000108f78, 0x1004685)
/usr/local/Cellar/go/1.16.6/libexec/src/runtime/debug/stack.go:24 +0x9f
runtime/debug.PrintStack()
/usr/local/Cellar/go/1.16.6/libexec/src/runtime/debug/stack.go:16 +0x25
main.Third(...)
/Users/corylanou/projects/gopherguides/learn/_training/fundamentals/errors/src/examples/stack/main.go:18
main.Second(...)
/Users/corylanou/projects/gopherguides/learn/_training/fundamentals/errors/src/examples/stack/main.go:14
main.First(...)
/Users/corylanou/projects/gopherguides/learn/_training/fundamentals/errors/src/examples/stack/main.go:10
main.main()
/Users/corylanou/projects/gopherguides/learn/_training/fundamentals/errors/src/examples/stack/main.go:6 +0x25Exercise (15 Min)
package main
import (
"errors"
"fmt"
"log"
)
var errInvalidKey = errors.New("invalid key")
type errNotFound struct {
key string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("unable to find value for %q", e.key)
}
func lookup(key string) (string, error) {
data := map[string]string{"foo": "bar", "mascot": "gopher", "go": "awesome"}
if key == "" {
return "", errInvalidKey
}
if v, ok := data[key]; ok {
return v, nil
}
return "", errNotFound{key: key}
}
// TODO: Convert the following error check to use errors.Is
func noKey() {
var key string
v, err := lookup(key)
if err != nil {
if err != errInvalidKey {
log.Fatal(err)
}
fmt.Println("please specify a valid key")
return
}
fmt.Printf("value for %q is %q", key, v)
}
// TODO: Convert the following error check to use errors.AsType (Go 1.26+)
func notFound() {
key := "baz"
v, err := lookup(key)
if err != nil {
if _, ok := err.(errNotFound); !ok {
log.Fatal(err)
}
fmt.Printf("error: %s\n", err)
return
}
fmt.Printf("value for %q is %q", key, v)
}
func found() {
key := "go"
v, err := lookup(key)
if err != nil {
if _, ok := err.(errNotFound); !ok {
log.Fatal(err)
}
fmt.Printf("error: %s\n", err)
return
}
fmt.Printf("value for %q is %q", key, v)
}
// TODO: Update the following function to print a stack trace when i == 10
func nested(i int) {
if i == 10 {
return
}
fmt.Println("nested call", i)
nested(i + 1)
}
func main() {
noKey()
notFound()
found()
nested(1)
}package main
import (
"errors"
"fmt"
"log"
)
var errInvalidKey = errors.New("invalid key")
type errNotFound struct {
key string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("unable to find value for %q", e.key)
}
func lookup(key string) (string, error) {
data := map[string]string{"foo": "bar", "mascot": "gopher", "go": "awesome"}
if key == "" {
return "", errInvalidKey
}
if v, ok := data[key]; ok {
return v, nil
}
return "", errNotFound{key: key}
}
// TODO: Convert the following error check to use errors.Is
func noKey() {
var key string
v, err := lookup(key)
if err != nil {
if err != errInvalidKey {
log.Fatal(err)
}
fmt.Println("please specify a valid key")
return
}
fmt.Printf("value for %q is %q", key, v)
}
// TODO: Convert the following error check to use errors.AsType (Go 1.26+)
func notFound() {
key := "baz"
v, err := lookup(key)
if err != nil {
if _, ok := err.(errNotFound); !ok {
log.Fatal(err)
}
fmt.Printf("error: %s\n", err)
return
}
fmt.Printf("value for %q is %q", key, v)
}
func found() {
key := "go"
v, err := lookup(key)
if err != nil {
if _, ok := err.(errNotFound); !ok {
log.Fatal(err)
}
fmt.Printf("error: %s\n", err)
return
}
fmt.Printf("value for %q is %q", key, v)
}
// TODO: Update the following function to print a stack trace when i == 10
func nested(i int) {
if i == 10 {
return
}
fmt.Println("nested call", i)
nested(i + 1)
}
func main() {
noKey()
notFound()
found()
nested(1)
}Solution
package main
import (
"errors"
"fmt"
"log"
"runtime/debug"
)
var errInvalidKey = errors.New("invalid key")
type errNotFound struct {
key string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("unable to find value for %q", e.key)
}
func lookup(key string) (string, error) {
data := map[string]string{"foo": "bar", "mascot": "gopher", "go": "awesome"}
if key == "" {
return "", errInvalidKey
}
if v, ok := data[key]; ok {
return v, nil
}
return "", errNotFound{key: key}
}
func noKey() {
var key string
v, err := lookup(key)
if err != nil {
if !errors.Is(err, errInvalidKey) {
log.Fatal(err)
}
fmt.Println("please specify a valid key")
return
}
fmt.Printf("value for %q is %q", key, v)
}
func notFound() {
key := "baz"
v, err := lookup(key)
if err != nil {
if _, ok := errors.AsType[errNotFound](err); !ok {
log.Fatal(err)
}
fmt.Printf("error: %s\n", err)
return
}
fmt.Printf("value for %q is %q", key, v)
}
func found() {
key := "go"
v, err := lookup(key)
if err != nil {
if _, ok := err.(errNotFound); !ok {
log.Fatal(err)
}
fmt.Printf("error: %s\n", err)
return
}
fmt.Printf("value for %q is %q", key, v)
}
// TODO: Update the following function to print a stack trace when i == 10
func nested(i int) {
if i == 10 {
debug.PrintStack()
return
}
fmt.Println("nested call", i)
nested(i + 1)
}
func main() {
noKey()
notFound()
found()
nested(1)
}package main
import (
"errors"
"fmt"
"log"
"runtime/debug"
)
var errInvalidKey = errors.New("invalid key")
type errNotFound struct {
key string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("unable to find value for %q", e.key)
}
func lookup(key string) (string, error) {
data := map[string]string{"foo": "bar", "mascot": "gopher", "go": "awesome"}
if key == "" {
return "", errInvalidKey
}
if v, ok := data[key]; ok {
return v, nil
}
return "", errNotFound{key: key}
}
func noKey() {
var key string
v, err := lookup(key)
if err != nil {
if !errors.Is(err, errInvalidKey) {
log.Fatal(err)
}
fmt.Println("please specify a valid key")
return
}
fmt.Printf("value for %q is %q", key, v)
}
func notFound() {
key := "baz"
v, err := lookup(key)
if err != nil {
if _, ok := errors.AsType[errNotFound](err); !ok {
log.Fatal(err)
}
fmt.Printf("error: %s\n", err)
return
}
fmt.Printf("value for %q is %q", key, v)
}
func found() {
key := "go"
v, err := lookup(key)
if err != nil {
if _, ok := err.(errNotFound); !ok {
log.Fatal(err)
}
fmt.Printf("error: %s\n", err)
return
}
fmt.Printf("value for %q is %q", key, v)
}
// TODO: Update the following function to print a stack trace when i == 10
func nested(i int) {
if i == 10 {
debug.PrintStack()
return
}
fmt.Println("nested call", i)
nested(i + 1)
}
func main() {
noKey()
notFound()
found()
nested(1)
}