Rror Interface Exactly
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
The interface that everything else stands on:
type error interface {
Error() string
}Any type with that method IS an error — no registration, no inheritance, no base class. errors.New's *errorString, fmt.Errorf's wrappers, your structs, third-party types: all just implement Error().
The important separation: the interface itself carries nothing but the message. Wrapping (%w), matching (errors.Is), type extraction (errors.As), chain traversal (Unwrap) are all behaviors of the errors package operating on errors that choose to participate — an error implements Unwrap() to join the wrapping machinery, Is(error) bool to customize matching, and so on. A bare struct with only Error() is a fully valid error; it just can't be wrapped-into or inspected beyond its message.
That minimalism is the design: one method makes errors universal (anything can fail with anything), and the optional protocol methods (Unwrap, Is, Unwrap []error) layer on capabilities without bloating the base contract. When you understand that the interface is just Error() and the errors package is a protocol on top, the whole error system stops being magic — it's interfaces all the way down, which is Go's answer to most questions.
Rror Interface Exactly
Direct Answer
One method: Error() string. Everything else — wrapping, Is, As, Unwrap — is the errors package's machinery built on top.
Deep Dive & Architecture
The interface's minimalism is a design decision with consequences worth tracing:
type error interface { Error() string }
ONE method means ANYTHING can be an error. A string wrapper (errors.New's *errorString), a formatted chain node (fmt.Errorf's *wrapError), your structs, a third-party HTTP error, a channel-closed sentinel — if it has Error() string, it flows through every return, every if err != nil, every errors.Is/As. No registration, no inheritance, no hierarchy to keep compatible. That's why error-returning code composes across packages that have never heard of each other.
But ONE method also means the interface itself carries NO structure: no chain, no cause, no code. Everything 'errors can do' beyond printing is PROTOCOL — optional methods the errors package probes for:
- Unwrap() error — chain edge. fmt.Errorf %w sets it; Is/As follow it.
- Unwrap() []error — branch edge (errors.Join); the tree-walk follows these too.
- Is(error) bool — custom class matching (os.ErrNotExist matches many filesystems' not-exist via this).
The errors package's Is/As are code that type-asserts for these methods. Your custom error participates in wrapping/matching to exactly the degree it implements the optional ones — a bare Error() struct is a fully valid error that Is/As see but can't see THROUGH.
That's the layered architecture: interface for universal compatibility, protocol for capability. Adding capability never breaks compatibility — old errors without the new methods simply don't offer the new behavior, nothing breaks. When you understand error as 'one method plus optional protocol,' the whole errors package stops being magic — it's plumbing over an interface any type can join.
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?
- How would you compare Custom Error Types Basics with an alternative?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.