Should Custom Errors Be Pointer Or Value
Fast recall
Quick revision
Core answer
Practical understanding of Custom Error Types Basics.
Repeating a definition without saying when to use it.
Give one trade-off and say how you would test it.
Interview-ready guide
Interview answer
Convention: pointer. Declare the type's Error() on the pointer, return &MyError{...}, and match with a pointer target in errors.As:
func (e *NotFoundError) Error() string { ... } // pointer receiver
return &NotFoundError{Name: name} // pointer value
var target *NotFoundError // pointer target
errors.As(err, &target)The receiver and the target must agree: As matches the dynamic type in the chain against the target's type. Pointer error, pointer target — match. Value error (Error() on the value receiver, stored as a value), value target — match. Mixing them is the silent bug: a pointer error against a value target compiles and always returns false, because a *NotFoundError is not assignable to NotFoundError.
Why pointer won as the convention: pointer receivers are Go's default for anything with identity semantics, they keep large error structs cheap to pass, they allow nil-ness (var x *MyError is nil before assignment), and one rule everywhere beats two rules matched by discipline. The standard library's typed errors (os.PathError, net.AddrError, etc.) are pointer-style. When in doubt, look at what the ecosystem does and do that — error code is read by strangers.
Should Custom Errors Be Pointer Or Value
Direct Answer
Pointer receivers, and errors.As with a pointer target — the ecosystem convention. Mixing value and pointer error types is the classic As-mismatch bug.
Deep Dive & Architecture
The pointer convention isn't style — it's what makes the As-matching machinery line up, and the mismatch is the classic silent bug.
The mechanics: errors.As(err, &target) walks the chain looking for an error assignable to the target's TYPE — the pointee of your pointer. A chain holding *NotFoundError (a pointer) matches a *NotFoundError target — the types agree. A value error (NotFoundError, no pointer) matches a value target. Cross them — pointer error, value target — and the assignability test fails for every node: *NotFoundError is not assignable to NotFoundError (different types), so As returns false forever. The code compiles (As takes any target), runs, and quietly never matches.
Why the ecosystem standardized on pointers:
- Methods on pointers are Go's default for anything with identity (every standard library error type: *os.LinkError, *net.AddrError, *time.ParseError — all pointers).
- The nil state: var target *NotFoundError starts nil — a clean 'not yet extracted' sentinel before As runs.
- Cheap passing: the error is one pointer wide, no matter the struct size.
- ONE convention beats two matched by discipline — every As is the same shape, and reviewers never check receiver-vs-target agreement.
The audit rule for a codebase mixing them: grep for value-receiver Error() methods — each is a latent As-mismatch for callers using the pointer idiom, or a signal the error was never meant for As. The fix is mechanical (add the star) but version-locked (value-errors in the wild change identity when converted) — which is why pointer-from-day-one is the cheaper habit.
Common Mistakes & Pitfalls
Repeating a definition without saying when to use it.
Check your understanding
Answer these without looking back at the article.
- What is Custom Error Types Basics, and when would you use it?
- When would you use Custom Error Types Basics in a real project?
- What is a common mistake when using Custom Error Types Basics?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.