skip to content

When does an error type need its own As(target any) bool method, and what must that method do?

level: middleimportance: should knowfreq 30%

answer

  1. only when you can produce what you do not hold
  2. assignability is tried first and usually suffices
  3. the parameter is a pointer to the caller's variable
  4. assign through it, then return true
  5. a true without an assignment stops the search

basics

~20 s

You need one only when callers must extract a type the chain does not contain, such as a view synthesised from your fields. The method must recognise the pointer it is handed, assign through it, then return true.

solid answer

~50 s

`errors.As` already finds any link whose dynamic type is assignable to the caller's target, so a plain typed error needs no `As` method at all. You write one when your type can *produce* something it does not hold — a proxy that can build a `*StatusError` view out of a raw upstream frame, or a package that must keep satisfying an older exported error type after its internals changed. The method receives the exact pointer the caller passed, so for `var se *StatusError; errors.As(err, &se)` the parameter holds a `**StatusError`. The body type-asserts for the pointer types it supports, assigns through the pointer, and returns `true` only on the branch where it actually assigned; returning true without assigning leaves the caller with a zero value and stops the search. Like `Is`, it must not unwrap — `errors.As` is already doing that.

code

go · 17 lines
go
type Frame struct{ Status int }

type ProxyError struct {
	Frame Frame
	Op    string
}

func (e *ProxyError) Error() string { return e.Op + ": upstream refused" }

func (e *ProxyError) As(target any) bool {
	t, ok := target.(**StatusError)
	if !ok {
		return false
	}
	*t = &StatusError{Status: e.Frame.Status, Op: e.Op}
	return true
}

go deeper

for a junior

Be ready to say that errors.As normally finds an error by type on its own, and that a type only needs its own As method in the unusual case where it must hand back something the chain does not contain.

for a middle

Explain the parameter: it is the caller's pointer, so extracting a *StatusError means switching on **StatusError, assigning through it, and returning true only then. Know that direct assignability is tried before your method.

for a senior

Show what you check in review or in production: whether every true branch assigns, whether a test asserts the extracted fields rather than the boolean, and what an over-eager method hides from callers downstream.

for a principal

Treat each supported case as a published contract. An As method that manufactures a legacy type is a migration tool with a cost: callers will keep depending on it, so decide up front how long you intend to keep the promise.

## What errors.As does without your help `errors.As(err, target)` walks the same chain `errors.Is` walks, and at each link asks a different question: is this link's dynamic type assignable to the type the target points at? If yes, it stores the link into `*target` and returns true. The caller writes: ```go var se *StatusError if errors.As(err, &se) { log.Printf("upstream status %d", se.Status) } ``` For the overwhelmingly common case — you return `*StatusError` somewhere in the chain and the caller wants it back — that is the whole story. No method is needed, and adding one is a way to introduce bugs for no benefit. ## When a method earns its place An `As` method exists so a type can be *treated as if it were a different error type*. Three shapes justify it: 1. **A synthesised view.** A proxy holds a raw protocol frame from an upstream vendor. Nothing in the chain is a `*StatusError`, but the frame contains everything needed to build one on demand. The `As` method constructs it and hands it over, so callers of your package never learn the vendor's frame type. 2. **Keeping an old type alive.** Your package once returned `*OldErr` and callers still extract it. Internally you now return something else. An `As` method on the new type can still produce an `*OldErr` view, so existing callers keep working while you migrate. 3. **Adapting a foreign shape.** An error from a system with its own error model can present itself as your package's type, so the rest of your code has one vocabulary to match on. If your reason is none of those — if the value the caller wants is genuinely in the chain — do not write the method. ## The exact contract The optional interface is `interface{ As(any) bool }`. Three rules govern the body. **Recognise the pointer, not the value.** `errors.As` passes through, unchanged, the pointer the caller gave it. If the caller declared `var se *StatusError` and called `errors.As(err, &se)`, your parameter's dynamic type is `**StatusError`. A type switch case written as `case *StatusError:` never fires, and the method silently returns false — a bug that looks like "errors.As just doesn't find it". **Assign, then return true.** The boolean is a promise that the target now holds something useful. `errors.As` trusts it and stops walking. A branch that returns true without writing through the pointer leaves the caller holding a nil or zero value, and the deeper link that could genuinely have matched is never reached. **Do not unwrap.** As with `Is`, the search already visits every link. Unwrapping inside the method duplicates the walk and can return a value from a link the search would not have reached yet. ```go func (e *ProxyError) As(target any) bool { t, ok := target.(**StatusError) if !ok { return false } *t = &StatusError{Status: e.Frame.Status, Op: e.Op} return true } ``` ## Interface targets A caller may target an interface rather than a concrete type: `var ne net.Error; errors.As(err, &ne)`. Then your parameter holds a `*net.Error`. If you support that, add the case explicitly; there is no automatic bridging from your concrete case to an interface one. `errors.As` itself panics if the target is nil, is not a pointer, or points at something that is neither an interface nor an implementation of `error` — those checks happen before the walk, so they are the caller's mistakes, not yours. ## Precedence: assignability wins At each link, `errors.As` tries plain assignability *before* calling that link's `As` method. So if your type is itself what the caller asked for, your method is never consulted for that target — a useful property, because it means adding an `As` method for a synthesised view cannot break direct extraction of your own type. ## A generic spelling of the same search Go 1.26 added `errors.AsType[E]`, a generic form that returns the matched error and a boolean instead of writing through a pointer. It performs the same search, so a custom `As` method still decides the outcome — switching to it does not rescue you from a method that lies. ## Reviewing one When a maintainer adds a new error type to an existing package, the review question is not "is the method correct" but "is it needed". If it is, check the two things that go wrong: does every `true` branch assign, and does every supported case switch on the double pointer? Then require a test that asserts the extracted value's fields, not just the boolean — that single assertion catches both mistakes.

  • A caller writes var se *StatusError; errors.As(err, &se). What is the dynamic type of the value your As method receives?
    `**StatusError` — `errors.As` passes the caller's pointer straight through, and the caller's variable is already a `*StatusError`. A type switch case written as `case *StatusError:` therefore never fires and the method returns false silently. Support the case you mean, `case **StatusError:`, and assign with `*t = ...`.
  • If a link's type is already assignable to the target, is that link's As method still called?
    No. At each link `errors.As` tries assignability first and, on success, stores the link and returns true. The `As` method is consulted only when the direct type match fails. That ordering is why adding an `As` method for a synthesised view cannot break callers who extract your concrete type directly.
  • Should an As method support an interface target such as net.Error?
    Only if you mean to. There is no automatic bridging: a `case **StatusError:` branch says nothing about a caller targeting an interface, whose pointer arrives as `*net.Error`. Add the case explicitly and assign a value that really satisfies the interface. Every case you add is a promise callers will depend on, so keep the supported set small and documented.

saying these in an interview costs you the question

  • Writes an As method when the type is already in the chain
  • Type-switches on *StatusError instead of **StatusError
  • Returns true from a branch that assigned nothing
  • Unwraps inside As instead of letting errors.As walk
  • Assumes the method must check the target for nil itself