Efine Error Types Instead Of Using Errors.new
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 sentinel (errors.New) gives the caller one bit: is it this error? The type gives the caller the error AND its specifics:
// sentinel — the caller learns WHICH failure, nothing else
var ErrRateLimited = errors.New("rate limited")
// type — the caller learns which failure and HOW LONG to wait
type RateLimitError struct{ RetryAfter time.Duration }The caller's next action is the whole point. With the sentinel, the caller can branch — 'if rate limited, use the backup' — but not act on specifics. With the type, the caller reads the fields and does something with them: sleep the RetryAfter, show the UserMessage, redirect to the Location, log the Which-Record.
When types become mandatory: the moment the caller's correct behavior depends on data about the failure, not just the category. Retry logic needs a duration. HTTP error responses need a status code. Partial-failure reporting needs which shard. String errors can't carry any of this — parsing the message is the forbidden alternative, because it couples the caller to your prose.
The progression in real codebases: start with errors.New sentinels, promote to a custom type when a caller first needs to ask a question the message can't answer structurally. The standard library made the same journey — os.PathError exists because 'which path?' is the first question every file-error caller asks.
Efine Error Types Instead Of Using Errors.new
Direct Answer
Typed errors carry data and match by type; string errors match by identity only. Types make the failure actionable.
Deep Dive & Architecture
The decision is about what the CALLER can do next — and the progression from sentinel to type is the story of every maturing codebase.
Sentinel (errors.New): the caller gets identity — a bit. 'Was it ErrNotFound?' — branch on it. What the caller CANNOT do: ask which record, what status, how long. The sentinel's message is prose; parsing prose is the forbidden path (it couples the caller to your wording).
Type (struct + Error()): the caller gets identity AND data. errors.As extracts the struct; the fields are machine-readable facts. The caller's next line uses them: sleep target.RetryAfter, render target.UserMessage, log target.Name.
The forcing function in real code: the first time a caller NEEDS a fact the message carries. A retry wrapper that needs to know the backoff duration can't branch on ErrRateLimited — it needs the duration. An HTTP error renderer that needs the status code can't parse 'bad request' — it needs the number. That moment — 'the handler needs the failure's details' — is the migration point, and it arrives predictably: retry logic, error responses, metrics with labels, partial-failure reports.
The standard library's own fossils tell the story: os.ErrPermission (sentinel — 'permission denied' is a complete answer) beside os.LinkError (type — Op, Old, New fields, because 'which link, which end?' is the first question a caller of symlink code asks). Both exist because both are right — the question decides the tool. The interview version: sentinels answer WHAT, types answer WHAT AND THE DETAILS; pick by whether the caller's next action needs the details.
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.