How does a value versus pointer receiver on Error() string change which errors.As target matches?
answer
- method sets decide who is an error
- matching is by assignability, not satisfaction
- value receiver means two possible forms
- the target must mirror what was returned
- guessing wrong returns false, not a panic
basics
~20 sA pointer receiver puts Error only in the method set of *T, so the package can only return *T and callers must target a *T variable. A value receiver lets both T and *T be errors, so the target must match whichever form the package actually returns.
solid answer
~40 s`errors.As` matches on assignability of the error's concrete dynamic type to the type the target points at, not on interface satisfaction. So the receiver decides the whole shape. With `func (e *ParseError) Error() string`, only `*ParseError` implements `error`; the package can only ever return `&ParseError{...}`, and the caller writes `var pe *ParseError; errors.As(err, &pe)`. With a value receiver, both `ParseError` and `*ParseError` implement `error`, so the package could return either — and a caller targeting `*ParseError` silently gets `false` when the package returned a value, and vice versa. That is why the convention is a pointer receiver plus always returning a pointer: it collapses the ambiguity to one form callers cannot get wrong.
code
go · 17 linestype ParseError struct{ Line int }
func (e ParseError) Error() string {
return fmt.Sprintf("parse failed at line %d", e.Line)
}
func report(err error) {
var pv ParseError
if errors.As(err, &pv) { // matches only a returned ParseError value
fmt.Println("value form, line", pv.Line)
}
var pp *ParseError
if errors.As(err, &pp) { // matches only a returned *ParseError
fmt.Println("pointer form, line", pp.Line)
}
}go deeper
Know the mechanical rule: look at what the function returns, declare a variable of exactly that type, and pass its address. If the package returns &T{}, your variable is a *T.
Explain both halves — which method set a value receiver versus a pointer receiver produces, and that errors.As matches by assignability of the dynamic type — and connect them to why one target silently fails.
Point out that a mismatched target fails silently rather than loudly, and describe how you would catch it: a test that drives a real failure through the exported function and asserts the typed branch fires.
Own the consistency rule across a codebase: one receiver convention for error types, enforced by generation or review, so no caller has to read docs to know whether a package returns a value or a pointer.
## Two rules meeting This question sits at the intersection of two independent Go rules, and it is confusing only until you separate them. **Rule one — method sets.** For a type `T` with a method declared on a value receiver, both `T` and `*T` have that method. For a method declared on a pointer receiver, only `*T` has it. So: | Receiver on `Error()` | Implements `error` | |---|---| | `func (e T) Error() string` | `T` and `*T` | | `func (e *T) Error() string` | `*T` only | **Rule two — how `errors.As` matches.** It does *not* ask "does this error satisfy the target's interface". It asks whether the concrete dynamic type stored in the error is **assignable** to the type the target points at. `*ParseError` is not assignable to `ParseError`, and `ParseError` is not assignable to `*ParseError`. They are different types, and the search simply does not match across them. ## What that means at the call site The caller must declare a variable of exactly the type the package returns, then pass its address: * Package returns `&ParseError{...}` (dynamic type `*ParseError`) → caller writes `var pe *ParseError` and passes `&pe`. * Package returns `ParseError{...}` (dynamic type `ParseError`) → caller writes `var pe ParseError` and passes `&pe`. Get it wrong and nothing crashes: both targets are legal (both `ParseError` and `*ParseError` implement `error` when the receiver is a value), so `errors.As` dutifully searches, finds nothing assignable, and returns `false`. The branch that should have produced a precise 422 with a line number quietly produces a generic 500 instead. A silent `false` is far more expensive to debug than a panic. ## Why the pointer receiver is the convention Declaring `Error()` on the pointer receiver removes the choice. `ParseError` no longer implements `error`, so the compiler rejects `return ParseError{...}` outright with a message noting that the method has a pointer receiver. There is exactly one dynamic type any caller can ever observe, and exactly one target shape that works. Secondary benefits: the struct is not copied every time something formats it, and the type can grow a field that must not be copied without the decision being revisited. A value receiver is not wrong in itself — some very small, deliberately copyable error types use one — but if you choose it, you take on the duty of returning one form consistently and documenting it, because the compiler will no longer enforce it for you. ## Generated code makes the choice once This is a strong argument for generating error types rather than hand-writing one per package. A generator that emits Go types from a schema emits the receiver, the constructor and the documented target shape together, so every generated package agrees. A codebase where half the packages return `MyErr` and half return `*MyErr` forces every caller to check the docs before writing an `errors.As`, and the failure mode for guessing wrong is invisible. ## The mirror-image mistake With a pointer-receiver type there is a second level of indirection that trips people up. The value you want is a `*ParseError`, so the *variable* is a `*ParseError` and the *target* is `&pe`, a `**ParseError`. Declaring `var pe ParseError` and passing `&pe` looks right and compiles — `ParseError` does not implement `error`, so this one actually panics rather than returning false, which is at least loud. Declaring the variable at the right level and passing its address is the habit to build. ## Quick check If you can answer "what is the dynamic type of the error this function returns", you can write the target mechanically: take that type, declare a variable of it, pass the address. Everything else about receivers follows from there.
- If Error() has a value receiver, can a caller still target *ParseError?Only if the package actually returned a pointer. Both forms implement `error`, so both targets compile and both searches run; the one that does not match the returned dynamic type simply reports false. That ambiguity is the cost of the value receiver, and the reason it has to be documented.
- Does errors.As match on interface satisfaction or on assignability?On assignability of the error's concrete dynamic type to the type the target points at — except when the target points at an interface type, in which case satisfying that interface is the test. `*ParseError` and `ParseError` are distinct types, so neither matches a target declared for the other.
- What does the compiler say if you return a value while Error() is on the pointer receiver?It rejects the return, reporting that the type does not implement `error` because method `Error` has a pointer receiver. That is the main practical benefit of the pointer receiver: the mistake becomes a build failure instead of a runtime search that silently finds nothing.
saying these in an interview costs you the question
- Assumes errors.As matches T and *T interchangeably
- Says a mismatched target panics rather than returning false
- Declares var pe ParseError when the package returns *ParseError
- Thinks the receiver choice only affects performance
- Believes errors.As tests interface satisfaction for concrete targets