Intermediate · Pointers, interfaces and errors · topic 19 of 46 · 40 min
Errors as values: wrap, Is and As
Explanation
Why errors as values?
Many languages throw exceptions that jump out of functions and are caught somewhere far away. It is easy to forget which calls can fail. Go makes failure visible: a function that can fail returns an error as its last result, and the caller decides what to do, right there. The code is longer, but the flow of control is always clear.
The error type
type error interface {
Error() string
}f, err := os.Open("marks.csv")
if err != nil {
return fmt.Errorf("open marks file: %w", err)
}
defer f.Close()Creating errors
import "errors"
var ErrNotFound = errors.New("not found") // a sentinel error
func findUser(id int) (string, error) {
if id != 1 {
return "", ErrNotFound
}
return "Asha", nil
}
err := fmt.Errorf("invalid amount %d", -5) // formatted, no wrappingWrapping with %w: adding context
A bare "not found" deep inside a big program is hard to debug. Each layer should add what it was doing. fmt.Errorf with the %w verb creates a new error whose message includes the old one AND keeps the old error inside, so it can still be found later. This is called wrapping.
func loadProfile(id int) error {
_, err := findUser(id)
if err != nil {
return fmt.Errorf("load profile %d: %w", id, err)
}
return nil
}
err := loadProfile(7)
fmt.Println(err) // load profile 7: not found
fmt.Println(errors.Is(err, ErrNotFound)) // trueCustom error types and errors.As
Sometimes a caller needs details: which field failed validation, which HTTP status the bank returned. Make a struct type with an Error method, return it (wrapped or not), and the caller extracts it with errors.As, which also looks through the chain.
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return e.Field + ": " + e.Msg
}
func validate(age int) error {
if age < 18 {
return &ValidationError{Field: "age", Msg: "must be 18 or older"}
}
return nil
}
err := fmt.Errorf("signup: %w", validate(15))
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("bad field:", ve.Field) // bad field: age
}Several errors at once
Go 1.20 added errors.Join(err1, err2, ...), which combines several errors into one (nil ones are dropped). errors.Is and errors.As check each of them. It is useful when validating a form with several bad fields.
Handle an error once
- Either handle it (retry, use a default, show a message) or return it with context. Do not log it AND return it at every layer, or one failure prints ten log lines.
- Never ignore errors silently with _ unless you have written down why it is safe.
- Do not check err.Error() text with strings.Contains; messages change. Use errors.Is or errors.As.
- Return early on errors so the "happy path" stays at the left edge of the code, not deeply nested.
// Hard to read: nested
if err == nil {
if data, err := parse(raw); err == nil {
save(data)
}
}
// Go style: return early
data, err := parse(raw)
if err != nil {
return fmt.Errorf("parse: %w", err)
}
return save(data)Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Errors as values: wrap, Is and As · 1/7
Go has no exceptions. Errors are ordinary values returned from functions.
Prefer a video by a teacher? Find free videos on “golang error handling wrapping errors.Is errors.As tutorial” on YouTube ↗
In real life: Handling a failed payment status check
A payment service calls a bank API. "Transaction not found yet" is normal right after paying and should be retried; a validation error should be shown to the user; anything else is logged as a real failure. Sentinel errors and custom types let the top-level code choose with errors.Is and errors.As, while lower layers just wrap and return.
package main
import (
"errors"
"fmt"
)
var ErrPending = errors.New("transaction pending")
type BankError struct{ Code int }
func (e *BankError) Error() string { return fmt.Sprintf("bank error %d", e.Code) }
func status(ref string) error {
switch ref {
case "T1":
return ErrPending
case "T2":
return &BankError{Code: 503}
}
return nil
}
func check(ref string) error {
if err := status(ref); err != nil {
return fmt.Errorf("check %s: %w", ref, err)
}
return nil
}
func main() {
for _, ref := range []string{"T1", "T2", "T3"} {
err := check(ref)
var be *BankError
switch {
case err == nil:
fmt.Println(ref, "success")
case errors.Is(err, ErrPending):
fmt.Println(ref, "pending, retry in 5s")
case errors.As(err, &be):
fmt.Println(ref, "bank down, code", be.Code)
default:
fmt.Println(ref, "failed:", err)
}
}
}Practice (0/4 done)
What does this print? err := fmt.Errorf("save: %w", io.EOF) fmt.Println(err == io.EOF, errors.Is(err, io.EOF))
What does this print? err := fmt.Errorf("save: %v", io.EOF) fmt.Println(errors.Is(err, io.EOF))
Which is the idiomatic error message?
Write func withdraw(balance, amount int64) (int64, error). Return a sentinel ErrInsufficient (wrapped with the amounts using %w) when amount > balance, and a *InvalidAmountError{Amount} when amount <= 0. In main, call it three times and use errors.Is and errors.As to print a different message for each case.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- panic and recover: only for the truly unexpectedPointers, interfaces and errors · same module
- Interfaces: behaviour, not family treesPointers, interfaces and errors · same module
- Go language interview questionsCareers and interviews
- Exceptions: try, catch and finallyJava Programming (Core to Spring Boot) · Exceptions and generics
- Checked vs unchecked exceptions and throwing your ownJava Programming (Core to Spring Boot) · Exceptions and generics
- Exceptions: try, catch, finally and custom exceptionsC# and .NET · Exceptions, delegates, events and async
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Which verb in fmt.Errorf wraps an error so errors.Is can find it?
Answer: %w
%w wraps; %v and %s only copy the text.
What does errors.As do?
Answer: Finds an error of a given type in the chain and assigns it to your variable
errors.As(err, &target) searches the chain for a matching type.
What is a sentinel error?
Answer: A package-level error value callers compare against, like io.EOF
Sentinels are predefined error values checked with errors.Is.
Which is the best practice?
Answer: Handle it once, or wrap and return it
Handle each error exactly once; otherwise wrap it with context and return it.
Which Go version added errors.Join?
Answer: 1.20
errors.Join arrived in Go 1.20, along with support for wrapping several errors.
Before Go 1.22, what did for i := 0; i < 3; i++ { defer func() { fmt.Print(i) }() } print?Related
Answer: 333
All closures shared one i, which was 3 when they ran. Since Go 1.22 it prints 210.
What happens to an exception nobody catches in main?Related
Answer: The thread ends and the stack trace is printed
Uncaught exceptions end the thread.
Why pass the original exception as a cause when wrapping?Related
Answer: So the root problem and its stack trace are not lost
Caused by: lines keep the full story.
A custom exception class should inherit from...Related
Answer: Exception
Custom exceptions derive from Exception (or a more specific exception).
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Pointers, interfaces and errors” before its exam: panic and recover: only for the truly unexpected.
- Module 4 exam: Pointers, interfaces and errors · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Packages, testing and generics. 46 topics left in the course before the final exam.