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
- Q319What does
fmt.Errorf("op: %w", err)return whenerris nil? Why does this matter in helper functions? - Q320Explain context errors:
context.Canceled,DeadlineExceeded, andcontext.Cause. - Q322What is "asserting errors for behavior"? How would you implement retry logic based on error behavior?
- Q323How does
net/httphandle a panic in a handler? What are the pitfalls? - Q324How would you design an error model for a large service, for example with Op, Kind and a wrapped cause?
- Q325What is the
io.Readererror contract, and why isif err != nil { return }wrong there?