Custom Errors & errors.Is/As

Because error is just an interface, you can create sentinel errors and custom error types, wrap them with %w to add context, and then inspect the chain with errors.Is and errors.As to react to exactly what went wrong.

Learn Custom Errors & errors.Is/As in our free Go course — an interactive lesson with runnable examples, a practice exercise and a quick reference.

Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Sentinel errors and errors.Is

A sentinel is a package-level error you compare against, created with errors.New . When you return it from deep in the call stack, wrap it with fmt.Errorf("...: %w", ErrNotFound) to add context while keeping it detectable. Callers use errors.Is , which walks the entire wrap chain — so it matches even through several layers.

2️⃣ Custom error types and errors.As

When the caller needs data about the failure — a field name, a code — define a struct and give it an Error() string method; that's all it takes to satisfy the error interface. To pull it back out of a wrap chain, use errors.As , which finds an error of that type and assigns it so you can read its fields.

3️⃣ Reacting to different failures

Real code uses several sentinels and routes on which one occurred — for example mapping internal errors to HTTP status codes. A switch with errors.Is cases reads cleanly and still works through wrapping.

🎯 Your Turn

Wrap a sentinel and then detect it. Fill in the two blanks marked ___ , then run it.

❌ Comparing wrapped errors with == — fails after %w .

❌ Wrapping with %v when you meant to keep the error detectable.

✅ Use %w so Is / As can still see the original.

❌ Passing a value instead of a pointer to errors.As — it panics ("target must be a non-nil pointer").

✅ Declare var e *MyError and call errors.As(err, &e) .

❌ Type-asserting the error ( err.(*MyError) ) — misses it when wrapped.

false — e is a new wrapper. But errors.Is(e, ErrA) is true , because it unwraps the chain.

errors.As — it extracts the typed error so you can read fields. errors.Is only answers yes/no for a value.

Make fetch return a wrapped *HTTPError with code 503, then let the errors.As block in main pull out and print just the code.

Practice quiz

What is the error interface in Go?

  • interface{ Error() string }
  • interface{ String() string }
  • interface{ Err() error }
  • a built-in struct type

Answer: interface{ Error() string }. Any type with an Error() string method satisfies the error interface.

Which verb wraps an error so errors.Is can still find it through the chain?

  • %v
  • %s
  • %w
  • %e

Answer: %w. %w in fmt.Errorf keeps a link to the original error; %v would flatten it to text and lose the link.

After err := fmt.Errorf("x: %w", ErrNotFound), what is err == ErrNotFound?

  • true
  • false
  • a compile error
  • a panic

Answer: false. Wrapping creates a new wrapper value, so == is false even though errors.Is(err, ErrNotFound) is true.

Which function detects a specific sentinel error value anywhere in the wrap chain?

  • errors.As
  • errors.Is
  • errors.Unwrap
  • errors.New

Answer: errors.Is. errors.Is walks the chain comparing against a value; use it for sentinels.

Which function extracts a typed error so you can read its fields?

  • errors.Is
  • errors.New
  • errors.As
  • fmt.Errorf

Answer: errors.As. errors.As finds an error of a given TYPE in the chain and assigns it to your variable.

How do you create a sentinel error?

  • var ErrX = errors.New("...")
  • var ErrX = fmt.Sprintf("...")
  • type ErrX error
  • var ErrX = panic("...")

Answer: var ErrX = errors.New("..."). A sentinel is a package-level value made with errors.New that callers compare against.

errors.As(err, target) requires target to be...

  • a value of the error type
  • a non-nil pointer to a variable of the error type
  • a string
  • nil

Answer: a non-nil pointer to a variable of the error type. Pass a pointer like &ve where var ve *MyError; a value (or nil) makes As panic.

Why use errors.Is instead of == for a wrapped sentinel?

  • == is slower
  • == fails after wrapping; Is unwraps the chain
  • == is not allowed on errors
  • they are identical

Answer: == fails after wrapping; Is unwraps the chain. After %w the value is a new wrapper, so == is false; errors.Is unwraps step by step and matches.

If you wrap with %v instead of %w, errors.Is(err, ErrX) will...

  • still return true
  • return false because the link is lost
  • panic
  • not compile

Answer: return false because the link is lost. %v only prints text and drops the chain link, so errors.Is can no longer find the original.

Is or As — you need the error's Code field?

  • errors.Is
  • errors.As
  • either works
  • neither

Answer: errors.As. errors.As extracts the typed error so you can read fields; errors.Is only gives a yes/no for a value.