Sentinel Errors
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 55 of 107.
A sentinel error is a predefined, package-level error variable that represents a specific error condition. Unlike creating new errors each time with errors.New, sentinel errors are declared once and reused throughout your code.
You define sentinel errors as package-level variables, typically with names starting with Err:
package user
import "errors"
var (
ErrNotFound = errors.New("user not found")
ErrInvalidEmail = errors.New("invalid email address")
ErrDuplicate = errors.New("user already exists")
)Functions return these predefined errors when specific conditions occur:
func FindUser(id int) (*User, error) {
user, exists := users[id]
if !exists {
return nil, ErrNotFound
}
return user, nil
}The power of sentinel errors is that callers can check for specific error conditions by comparing error values directly:
user, err := FindUser(42)
if err == ErrNotFound {
fmt.Println("Creating new user...")
} else if err != nil {
fmt.Println("Unexpected error:", err)
}Go's standard library uses sentinel errors extensively. For example, io.EOF signals the end of input, and sql.ErrNoRows indicates an empty query result. This pattern creates a clear contract between packages and their users about what errors to expect.
However, simple equality checks break when errors are wrapped. In the next lesson, you'll learn how errors.Is() solves this problem by checking the entire error chain.
Challenge
EasyLet's build an inventory management system that uses sentinel errors to communicate specific failure conditions. You'll define package-level error variables that callers can check to handle different scenarios appropriately.
You'll organize your code across two files:
inventory.go: Create an inventory system with predefined sentinel errors for common failure cases.Define three sentinel errors at the package level:
ErrItemNotFoundwith message"item not found in inventory"ErrOutOfStockwith message"item is out of stock"ErrInsufficientQuantitywith message"insufficient quantity available"
Create an
Inventorystruct that holds a map of item names to their quantities. Implement these methods:NewInventory() *Inventory- creates an empty inventoryAddItem(name string, quantity int)- adds or updates an item's quantityGetQuantity(name string) (int, error)- returns the quantity for an item, orErrItemNotFoundif it doesn't existPurchase(name string, quantity int) error- attempts to purchase items. ReturnErrItemNotFoundif the item doesn't exist,ErrOutOfStockif the current quantity is zero, orErrInsufficientQuantityif the requested amount exceeds available stock. On success, reduce the inventory and returnnil.
main.go: Read inventory operations from input and demonstrate how callers can check for specific sentinel errors using direct equality comparison.Read the item name and purchase quantity from input. Create an inventory, add the item
"laptop"with quantity5, then attempt the purchase. Based on the error returned, print a specific message showing that you identified which sentinel error occurred.
The following inputs will be provided:
- Line 1: Item name to purchase
- Line 2: Quantity to purchase
Handle the result by checking which specific error was returned:
- If
err == ErrItemNotFound: printError: Item '[name]' does not exist in our inventory - If
err == ErrOutOfStock: printError: Item '[name]' is currently out of stock - If
err == ErrInsufficientQuantity: printError: Cannot purchase [quantity] units of '[name]' - not enough in stock - If successful: print
Successfully purchased [quantity] units of '[name]'
For example, given laptop and 3, your output should be:
Successfully purchased 3 units of 'laptop'And given phone and 2, your output should be:
Error: Item 'phone' does not exist in our inventoryAnd given laptop and 10, your output should be:
Error: Cannot purchase 10 units of 'laptop' - not enough in stockCheat sheet
A sentinel error is a predefined, package-level error variable that represents a specific error condition. Sentinel errors are declared once and reused throughout your code.
Define sentinel errors as package-level variables, typically with names starting with Err:
package user
import "errors"
var (
ErrNotFound = errors.New("user not found")
ErrInvalidEmail = errors.New("invalid email address")
ErrDuplicate = errors.New("user already exists")
)Functions return these predefined errors when specific conditions occur:
func FindUser(id int) (*User, error) {
user, exists := users[id]
if !exists {
return nil, ErrNotFound
}
return user, nil
}Callers can check for specific error conditions by comparing error values directly:
user, err := FindUser(42)
if err == ErrNotFound {
fmt.Println("Creating new user...")
} else if err != nil {
fmt.Println("Unexpected error:", err)
}Go's standard library uses sentinel errors extensively, such as io.EOF and sql.ErrNoRows.
Try it yourself
package main
import (
"fmt"
)
func main() {
// Read input
var itemName string
var quantity int
fmt.Scanln(&itemName)
fmt.Scanln(&quantity)
// Create inventory and add "laptop" with quantity 5
inv := NewInventory()
inv.AddItem("laptop", 5)
// TODO: Attempt to purchase the item
// Call inv.Purchase(itemName, quantity)
// TODO: Check which specific sentinel error was returned using direct equality comparison
// Use if/else if statements to check:
// - err == ErrItemNotFound
// - err == ErrOutOfStock
// - err == ErrInsufficientQuantity
// - err == nil (success)
// TODO: Print the appropriate message based on the error
// If ErrItemNotFound: fmt.Printf("Error: Item '%s' does not exist in our inventory\n", itemName)
// If ErrOutOfStock: fmt.Printf("Error: Item '%s' is currently out of stock\n", itemName)
// If ErrInsufficientQuantity: fmt.Printf("Error: Cannot purchase %d units of '%s' - not enough in stock\n", quantity, itemName)
// If success: fmt.Printf("Successfully purchased %d units of '%s'\n", quantity, itemName)
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool