0Pricing
Go Academy · Lesson

Creating Custom Errors

errors.New, fmt.Errorf, and sentinel errors

Creating Custom Errors is a free Go Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Go Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Custom Error Types?

Custom error types carry structured data beyond a string message, enabling callers to inspect and react to specific error conditions:

package main
import "fmt"

// Custom error type carries extra context
type HTTPError struct {
    StatusCode int
    Message    string
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message)
}

func fetch(url string) error {
    return &HTTPError{404, "page not found"}
}

func main() {
    err := fetch("http://example.com")
    fmt.Println(err) // HTTP 404: page not found
}

Implementing the error Interface

Any type with an Error() string method satisfies error:

package main
import "fmt"

type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: field %q value %v — %s", e.Field, e.Value, e.Message)
}

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{"age", age, "must be between 0 and 150"}
    }
    return nil
}

func main() {
    fmt.Println(validateAge(-5))
}

Sentinel Errors with errors.New

Sentinel errors are package-level variables for well-known error conditions:

package main
import ("fmt"; "errors")

var (
    ErrNotFound      = errors.New("not found")
    ErrUnauthorized  = errors.New("unauthorized")
    ErrAlreadyExists = errors.New("already exists")
)

func getUser(id int) (string, error) {
    if id != 1 { return "", ErrNotFound }
    return "Alice", nil
}

func main() {
    _, err := getUser(99)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("user does not exist")
    }
}

fmt.Errorf with %w for Wrapping

fmt.Errorf with %w wraps an error, preserving the original for errors.Is and errors.As:

package main
import ("fmt"; "errors")

var ErrDatabase = errors.New("database error")

func queryDB() error {
    return fmt.Errorf("queryDB: %w", ErrDatabase)
}

func main() {
    err := queryDB()
    fmt.Println(err)                       // queryDB: database error
    fmt.Println(errors.Is(err, ErrDatabase)) // true — unwrapped
}

Custom Error with Unwrap

Implement Unwrap() error on your custom error type to participate in error chains:

package main
import ("fmt"; "errors")

var ErrNotFound = errors.New("not found")

type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string {
    return fmt.Sprintf("query %q: %v", e.Query, e.Err)
}
func (e *QueryError) Unwrap() error { return e.Err }

func main() {
    err := &QueryError{"SELECT *", ErrNotFound}
    fmt.Println(err)
    fmt.Println(errors.Is(err, ErrNotFound)) // true
}

errors.Is — Identity Check

errors.Is walks the error chain looking for an error equal to the target:

package main
import ("fmt"; "errors")

var ErrTimeout = errors.New("timeout")

func deepCall() error {
    return fmt.Errorf("layer3: %w", fmt.Errorf("layer2: %w", ErrTimeout))
}

func main() {
    err := deepCall()
    fmt.Println(errors.Is(err, ErrTimeout)) // true — found deep in chain
}

errors.As — Type Extraction

errors.As finds the first error in the chain matching a target type and assigns it:

package main
import ("fmt"; "errors")

type PermissionError struct{ User string }
func (e *PermissionError) Error() string { return "permission denied for " + e.User }

func action(user string) error {
    return fmt.Errorf("action: %w", &PermissionError{user})
}

func main() {
    err := action("bob")
    var pe *PermissionError
    if errors.As(err, &pe) {
        fmt.Println("denied:", pe.User) // denied: bob
    }
}

Multiple Error Wrapping (Go 1.20+)

Go 1.20 added errors.Join and fmt.Errorf with multiple %w for joining errors:

package main
import ("fmt"; "errors")

func validate(name, email string) error {
    var errs []error
    if name == ""  { errs = append(errs, errors.New("name required")) }
    if email == "" { errs = append(errs, errors.New("email required")) }
    return errors.Join(errs...)
}

func main() {
    err := validate("", "")
    fmt.Println(err)
    // name required
    // email required
}

Error Type Naming Conventions

Conventions for error variables and types in Go:

  • Sentinel error variables: ErrXxx (e.g. ErrNotFound)
  • Custom error types: XxxError (e.g. ValidationError)
  • Package-level var for sentinel, pointer type for rich errors
  • Exported for callers to use in errors.Is / errors.As

When to Use Each Approach

Choosing the right error approach:

  • errors.New — simple, no extra data, sentinel identity check
  • fmt.Errorf — formatted message with optional wrapping
  • Custom type — when caller needs structured data (fields, codes)
  • errors.Join — aggregate multiple validation errors

Quick Check

Which function allows extracting a specific error type from an error chain?

Recap: Custom Errors

Summary:

  • Implement Error() string to create custom error types
  • Add Unwrap() error to participate in error chains
  • Use errors.Is for identity checks, errors.As for type extraction
  • Sentinel errors: ErrXxx; custom types: XxxError
  • errors.Join aggregates multiple errors (Go 1.20+)

Frequently asked questions

Is the “Creating Custom Errors” lesson free?

Yes — the full text of “Creating Custom Errors” is free to read here on the web, and the Go Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Go Academy course, upgrade to CoddyKit PRO.

What will I learn in “Creating Custom Errors”?

errors.New, fmt.Errorf, and sentinel errors You practise Go Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Go Academy?

No prior experience is required. Go Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Creating Custom Errors” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Go Academy lesson?

Yes. Every Go Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. The error Interface
  2. Creating Custom Errors
  3. Error Wrapping and Unwrapping
  4. panic, recover and defer
← Back to Go Academy