Errors: Wrapping, Is & As
Go has no exceptions and no try/catch. Instead, a function that can fail returns an error value, and the caller decides what to do with it. This makes failure paths visible in the code: you can see exactly where each error is checked.
In this lesson you will create errors, add context by wrapping them, inspect wrapped errors with errors.Is, errors.As and the newer errors.AsType, define your own error types, combine several errors, and learn the small role panic and recover play.
The error type
error is a built-in interface (a type defined by its methods, covered in Interfaces). Any type with a method Error() string is an error. The two simplest ways to make one are errors.New for a fixed message and fmt.Errorf for a formatted one.
By convention, error messages start with a lower-case letter and have no full stop at the end, because they are often joined with other messages. The usual check looks like this:
qty, err := strconv.Atoi(input)
if err != nil {
return fmt.Errorf("read quantity: %w", err)
}
// use qty hereHandle the error first and keep the success path unindented, as you saw with guard clauses in if & switch.
Sentinel errors and wrapping with %w
A sentinel error is an error stored in a package-level variable so callers can recognise it. Its name starts with Err. As an error travels up through function calls, each level can wrap it with more context using the %w verb in fmt.Errorf. The original error stays inside, like a letter in an envelope.
package main
import (
"errors"
"fmt"
)
var ErrOutOfStock = errors.New("out of stock")
func reserve(item string, available int) error {
if available == 0 {
return ErrOutOfStock
}
return nil
}
func placeOrder(id int, item string, available int) error {
if err := reserve(item, available); err != nil {
return fmt.Errorf("order %d: reserve %s: %w", id, item, err)
}
return nil
}
func main() {
err := placeOrder(42, "cricket bat", 0)
fmt.Println(err)
fmt.Println(err == ErrOutOfStock)
fmt.Println(errors.Is(err, ErrOutOfStock))
fmt.Println(errors.Unwrap(err) == ErrOutOfStock)
}order 42: reserve cricket bat: out of stock false true true
The message reads like a trail: which order, which step, what went wrong. Plain == fails because err is now the wrapper, not the sentinel. errors.Is unwraps layer by layer and finds the match, so always use errors.Is to check for a specific error. errors.Unwrap removes exactly one layer.
Custom error types with As and AsType
Sometimes the caller needs data from an error, not just its identity: which amount failed, which field was wrong. Define your own type with an Error method. The (e *PaymentError) part before the method name is a receiver, which attaches the method to the type; Structs & Methods explains it fully.
package main
import (
"errors"
"fmt"
)
type PaymentError struct {
Amount int
Reason string
}
func (e *PaymentError) Error() string {
return fmt.Sprintf("payment of ₹%d failed: %s", e.Amount, e.Reason)
}
func checkout(amount int) error {
if amount > 5000 {
err := &PaymentError{Amount: amount, Reason: "daily limit exceeded"}
return fmt.Errorf("checkout: %w", err)
}
return nil
}
func main() {
err := checkout(7500)
fmt.Println(err)
var pe *PaymentError
if errors.As(err, &pe) {
fmt.Println("Reason:", pe.Reason)
}
if pe, ok := errors.AsType[*PaymentError](err); ok {
fmt.Println("Amount:", pe.Amount)
}
}checkout: payment of ₹7500 failed: daily limit exceeded Reason: daily limit exceeded Amount: 7500
errors.As searches the chain for an error of the target's type and, if found, stores it in the variable you pass by address (&pe). errors.AsType, added in Go 1.26, does the same job with generics: you name the type in square brackets and get the value and a bool back. It needs no separate variable and cannot be called with the wrong kind of argument, so prefer it in new code.
Joining several errors
Form validation often finds more than one problem. errors.Join (Go 1.20) combines errors into one. It skips nil values and returns nil if all of them are nil. The combined message puts each error on its own line, and errors.Is and errors.As still find every one inside.
package main
import (
"errors"
"fmt"
)
var ErrNameMissing = errors.New("name is required")
func validate(name string, age int) error {
var nameErr, ageErr error
if name == "" {
nameErr = ErrNameMissing
}
if age < 18 {
ageErr = fmt.Errorf("age %d is below 18", age)
}
return errors.Join(nameErr, ageErr)
}
func main() {
err := validate("", 16)
fmt.Println(err)
fmt.Println(errors.Is(err, ErrNameMissing))
fmt.Println(validate("Meera", 22) == nil)
}name is required age 16 is below 18 true true
panic and recover
A panic stops the normal flow of a program. The runtime panics on bugs such as an index out of range or a division by zero, and you can call panic("message") yourself for situations that should be impossible. An unhandled panic prints the message and a stack trace, and the program exits with status 2.
recover, called inside a deferred function, stops a panic and returns its value. Servers use it so that one bad request cannot crash the whole program:
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil
}
func main() {
fmt.Println(safeDivide(10, 2))
fmt.Println(safeDivide(1, 0))
}5 <nil> 0 recovered: runtime error: integer divide by zero
The deferred function runs while the panic unwinds, sets the named result err, and the function returns normally. Do not use panic for ordinary failures like a missing file or bad user input; return an error instead. Panics are for programmer mistakes.
Common mistakes
- Comparing wrapped errors with
==. Useerrors.Is(err, ErrX). - Wrapping with
%vinstead of%w. The cause is lost toerrors.Isanderrors.As. - Logging and returning the same error. It gets printed several times. Either handle it (log it) or return it with context, not both.
- Capitalised messages with full stops. Write
"open config: no such file", not"Open config: No such file.".
Key points
- Functions return
erroras their last result;nilmeans success. - Add context with
fmt.Errorf("doing x: %w", err);%wkeeps the original error inside. errors.Isfinds a specific error value anywhere in the chain.errors.Asand the genericerrors.AsType(Go 1.26) find an error of a specific type and give you its fields.errors.Joincombines several errors;panic/recoverare for bugs, not normal failures.
Exercise
Create a sentinel error ErrInsufficientBalance. Write withdraw(balance, amount int) (int, error) that returns the sentinel when amount is larger than balance, and the new balance otherwise. Write atmWithdraw(card string, balance, amount int) error that wraps any error with the card number. In main, try withdrawing ₹3,000 from a balance of ₹2,500 for card XXXX-1234 and print the error and whether errors.Is matches the sentinel.
Show solution
package main
import (
"errors"
"fmt"
)
var ErrInsufficientBalance = errors.New("insufficient balance")
func withdraw(balance, amount int) (int, error) {
if amount > balance {
return balance, ErrInsufficientBalance
}
return balance - amount, nil
}
func atmWithdraw(card string, balance, amount int) error {
if _, err := withdraw(balance, amount); err != nil {
return fmt.Errorf("card %s: withdraw ₹%d: %w", card, amount, err)
}
return nil
}
func main() {
err := atmWithdraw("XXXX-1234", 2500, 3000)
fmt.Println(err)
fmt.Println(errors.Is(err, ErrInsufficientBalance))
}card XXXX-1234: withdraw ₹3000: insufficient balance true
The %w verb keeps the sentinel inside the wrapper, so errors.Is still recognises it after the card number and amount were added.
Quick Quiz
1. Which verb in fmt.Errorf keeps the original error available to errors.Is?
%w wraps the error; %v and %s copy only its text.
2. err wraps ErrNotFound. What does err == ErrNotFound return?
The wrapper is a different value. Use errors.Is(err, ErrNotFound), which looks inside.
3. What does errors.AsType[*PaymentError](err) return?
AsType (Go 1.26) returns the first error of that type in the chain and whether one was found.
4. When should Go code use panic?
Normal failures are returned as errors; panics are for bugs.
Learn Go with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
