Advanced

Error Handling

Handle failures explicitly with the error interface and helper constructors.

What you will see

failed: not found

About 2 min of reading

Try It

You will create and return errors as values, wrap them with context, and use Is and As to inspect the failure before deciding how to react.

1

I return a value and an error together. When the guard fails, errors.New builds a simple error message and the caller prints both the zero value and the error.

package main import ( "errors" "fmt" ) func check(age int) (int, error) { if age < 0 { return 0, errors.New("age cannot be negative") } return age, nil } func main() { v, err := check(-1) fmt.Println(v, err) }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
0 age cannot be negative
2

I wrap an error with extra context using Errorf. The format string explains where the failure happened, and the caller prints the full message.

package main import "fmt" func readFile(path string) ([]byte, error) { return nil, fmt.Errorf("reading %s: file missing", path) } func main() { _, err := readFile("data.txt") fmt.Println(err) }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
reading data.txt: file missing
Use the %v verb only when you do not need to keep the error chain. %w wraps the original error so it can be inspected later.
3

I define a named sentinel error and return it when the lookup fails. The caller uses errors.Is to match that specific error value.

package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func find(id int) error { if id < 0 { return ErrNotFound } return nil } func main() { err := find(-1) fmt.Println(errors.Is(err, ErrNotFound)) }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
true
Sentinel errors work best when the exact same error value is shared, which is why they are usually declared at package level.
4

I define a custom error type with an extra field and use errors.As to recover it. The two-value form extracts the concrete type without panicking.

package main import ( "errors" "fmt" "strconv" ) type MyError struct { Code int } func (e *MyError) Error() string { return "error code " + strconv.Itoa(e.Code) } func boom() error { return &MyError{Code: 42} } func main() { err := boom() var myErr *MyError if errors.As(err, &myErr) { fmt.Println("code:", myErr.Code) } }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
code: 42
errors.As needs a pointer to the target variable so it can fill in the concrete error type after confirming it matches.
5

I combine both patterns: the function returns a sentinel error, and the caller checks it with a guard before continuing. The failure path prints the error and returns early.

package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func find(id int) (string, error) { if id < 0 { return "", ErrNotFound } return "found", nil } func main() { v, err := find(-1) if err != nil { fmt.Println("failed:", err) return } fmt.Println(v) }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
failed: not found

Data Flow

A function returns a value and an error; the caller unwraps and inspects it.
1 Function detects a failure
↓
2 It builds or wraps an error
↓
3 Caller checks err != nil
↓
4 Is/As extract details for handling
Why this works
The error interface

An error is any type with a Error() string method:

type error interface {
    Error() string
}

The standard library and your own code all implement this. Returning nil means success.

Is and As

Two helpers inspect error chains:

errors.Is(err, os.ErrNotExist)  // match by value
var target *PathError
errors.As(err, &target)         // match by type

Is compares errors (great with sentinel errors). As extracts a concrete typed error for further use.

Examples

Creating errors

Making errors

import "errors"

if age < 0 {
    return 0, errors.New("age cannot be negative")
}

errors.New builds a simple error from a string. Functions return it alongside their value so callers can react.

Why it works: Go treats errors as values you check, not exceptions that jump around.

Wrapping errors

Adding context

import "fmt"

data, err := readFile(path)
if err != nil {
    return fmt.Errorf("reading %s: %w", path, err)
}

%w wraps the original error. Later code can inspect it with errors.Is or errors.As.

Watch out: use %w, not %v, when you want the chain preserved.

Checking error types

Distinguishing errors

var *os.PathError
if perr, ok := err.(*os.PathError); ok {
    fmt.Println("file problem:", perr.Path)
}

errors.As type-asserts into a pointer. Use it when you need details specific to an error kind.

Why it works: custom error types carry extra information beyond the message.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Interfaces
Previous Lesson
Interfaces
Next Lesson
Goroutines and Concurrency
Goroutines and Concurrency →