Go

Why is errors.Is(err, fs.ErrNotExist) preferred over os.IsNotExist(err)?

Question 321MediumGo 1.22 to 1.25

os.IsNotExist, os.IsExist and os.IsPermission were written before error wrapping existed. They unwrap only a fixed set of types (*PathError, *LinkError, *SyscallError) and don't follow Unwrap chains. Once your code wraps the error with %w, they return false.

_, err := os.Open("/does/not/exist")
wrapped := fmt.Errorf("load config: %w", err)

fmt.Println(os.IsNotExist(err))                    // true
fmt.Println(os.IsNotExist(wrapped))                // false ← gotcha
fmt.Println(errors.Is(wrapped, fs.ErrNotExist))    // true
fmt.Println(errors.Is(wrapped, os.ErrNotExist))    // true (same value)

This works because syscall.Errno implements an Is method that maps ENOENT to fs.ErrNotExist. That is a good real-world example of a custom Is: platform-specific error codes matched against portable sentinels.

The general lesson: prefer errors.Is/errors.As over older IsXxx helpers and direct type assertions such as err.(*os.PathError), which also fail on wrapped errors.

More on Error Handling & panics

All 37 Error Handling & panics questions