errors.Is() and errors.As()
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 56 of 107.
When errors get wrapped, simple equality checks like err == ErrNotFound no longer work. The errors package provides two functions to inspect wrapped error chains: errors.Is() and errors.As().
errors.Is() checks if any error in the chain matches a specific sentinel error:
import "errors"
var ErrNotFound = errors.New("not found")
func FindUser(id int) error {
return fmt.Errorf("database lookup failed: %w", ErrNotFound)
}
err := FindUser(42)
if errors.Is(err, ErrNotFound) {
fmt.Println("User doesn't exist")
}Even though the returned error is wrapped, errors.Is() unwraps the chain and finds ErrNotFound inside.
errors.As() checks if any error in the chain matches a specific type and extracts it:
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "validation failed: " + e.Field
}
err := fmt.Errorf("request failed: %w", &ValidationError{Field: "email"})
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("Invalid field:", ve.Field)
}Use errors.Is() when checking for sentinel errors, and errors.As() when you need to access fields from a custom error type. Both functions traverse the entire error chain, making them essential for handling wrapped errors properly.
Challenge
EasyLet's build a file processing system that demonstrates how to properly inspect wrapped error chains using errors.Is() and errors.As(). You'll create errors that get wrapped as they pass through different layers, then use these functions to identify and extract information from them.
You'll organize your code across two files:
errors.go: Define the error infrastructure for your file system.Create a sentinel error:
ErrPermissionDeniedwith message"permission denied"
Create a custom error type:
FileErrorstruct withFilename(string) andOperation(string) fields- Its
Error()method should return:[Operation] failed on file: [Filename]
Implement two functions that simulate file operations and wrap errors:
ReadFile(filename string) error- If filename is"secret.txt", returnErrPermissionDenied. If filename is"missing.txt", return a*FileErrorwith Operation"read"and the given Filename. Otherwise returnnil.ProcessFile(filename string) error- CallsReadFile. If an error is returned, wrap it usingfmt.Errorfwith the format"processing failed: %w". Otherwise returnnil.
main.go: Read a filename from input and callProcessFile. Useerrors.Is()anderrors.As()to inspect the wrapped error chain and print appropriate messages based on what you find inside.
The following input will be provided:
- Line 1: Filename to process
Handle the result by inspecting the error chain:
- If
errors.Is()findsErrPermissionDeniedin the chain: print the full error message, then on a new line printAccess denied - check file permissions - If
errors.As()finds a*FileErrorin the chain: print the full error message, then on a new line printFile issue: [Filename] during [Operation] - If no error occurs: print
File '[filename]' processed successfully
For example, given secret.txt, your output should be:
processing failed: permission denied
Access denied - check file permissionsAnd given missing.txt, your output should be:
processing failed: read failed on file: missing.txt
File issue: missing.txt during readAnd given data.txt, your output should be:
File 'data.txt' processed successfullyNotice how even though the errors are wrapped by ProcessFile, you can still detect the original sentinel error with errors.Is() and extract the custom error type with errors.As(). These functions traverse the entire error chain to find what you're looking for.
Cheat sheet
When errors are wrapped, use errors.Is() and errors.As() from the errors package to inspect the error chain.
errors.Is() checks if any error in the chain matches a sentinel error:
import "errors"
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("database lookup failed: %w", ErrNotFound)
if errors.Is(err, ErrNotFound) {
// ErrNotFound was found in the chain
}errors.As() checks if any error in the chain matches a specific type and extracts it:
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "validation failed: " + e.Field
}
err := fmt.Errorf("request failed: %w", &ValidationError{Field: "email"})
var ve *ValidationError
if errors.As(err, &ve) {
// ve now contains the ValidationError from the chain
fmt.Println(ve.Field)
}Use errors.Is() for sentinel errors and errors.As() when you need to access fields from custom error types. Both functions traverse the entire error chain.
Try it yourself
package main
import (
"errors"
"fmt"
)
func main() {
// Read the filename from input
var filename string
fmt.Scanln(&filename)
// Call ProcessFile with the filename
err := ProcessFile(filename)
// TODO: Handle the result by inspecting the error chain
//
// Use errors.Is() to check if ErrPermissionDenied is in the chain:
// - If found: print the full error, then "Access denied - check file permissions"
//
// Use errors.As() to check if a *FileError is in the chain:
// - If found: print the full error, then "File issue: [Filename] during [Operation]"
//
// If no error: print "File '[filename]' processed successfully"
_ = errors.Is // hint: use errors.Is()
_ = errors.As // hint: use errors.As()
_ = err
}
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