skip to content

What must the target argument to errors.As be, and what happens if it is not a pointer?

level: middleimportance: must knowfreq 66%

answer

  1. the second argument gets written to
  2. it needs somewhere to put the match
  3. a pointer, and never nil
  4. wrong shape panics, it does not return false
  5. go vet has a check named for it

basics

~20 s

The target must be a non-nil pointer to a type implementing error, or to an interface type. errors.As writes the match through it, so anything else, including a plain value or a nil pointer, panics at runtime instead of returning false.

solid answer

~40 s

`errors.As(err, target)` searches `err` and everything it wraps, and if it finds an error whose concrete type is assignable to `*target`, it assigns it and returns true. Because it writes through the target, the target has to be a non-nil pointer to a type that implements `error` (typically `**FieldError`, obtained as `&fe` from `var fe *FieldError`) or a pointer to an interface type. Any other target is a programming mistake, and `errors.As` panics rather than quietly returning false. The nasty part is that the panic only fires on the error path, so a happy-path test suite never sees it. `go vet`'s `errorsas` check flags the bad call at build time, and it is one of the checks `go test` runs by default.

code

go · 17 lines
go
// Correct: a pointer to a *FieldError variable.
func status(err error) int {
	var fe *FieldError
	if errors.As(err, &fe) {
		return 422
	}
	return 500
}

// Panics on the first failing input: fe is a value, not a pointer.
func brokenStatus(err error) int {
	var fe FieldError
	if errors.As(err, fe) {
		return 422
	}
	return 500
}

go deeper

for a junior

Memorise the calling shape: declare var fe *MyErr, pass &fe, check the bool, then read the fields. Know that the second argument must always be an address, never the value itself.

for a middle

Explain why the target is an out-parameter at all, name the three rules it must satisfy, and say why a violation panics instead of returning false.

for a senior

Show awareness that the panic hides on the error path until production, and describe the defences: go vet's errorsas in the build, plus a test that actually exercises a failing input end to end.

for a principal

Own the policy angle: which vet checks are mandatory in CI, whether generated packages are held to the same gate as handwritten ones, and when a toolchain new enough for the generic alternative is worth requiring.

## The function ```go func As(err error, target any) bool ``` `errors.As` answers the question "is there an error of *this concrete type* anywhere in this error, and if so, hand it to me". It is the typed counterpart to identity matching: instead of asking whether the error *is* one particular value, you ask whether some error in the chain has a type you can read fields off. ## What it does It walks the error, then the error that one wraps, and so on. At each step it asks whether the error's concrete type is **assignable** to the type that `target` points at. On the first match it performs the assignment and returns `true`. If it runs off the end of the chain it returns `false` and leaves the target untouched. That assignment is the reason for the whole target convention. `errors.As` has no other way to hand you a value of a type it does not know at compile time, so you give it the address of a variable of the type you want and it fills it in — an out-parameter, the same shape as `encoding/json`'s `Unmarshal`. ```go var fe *FieldError if errors.As(err, &fe) { // fe now points at the FieldError found in the chain return badRequest(fe.Field) } ``` Note the double indirection: the value you want is a `*FieldError`, so the variable is a `*FieldError` and the target is `&fe`, a `**FieldError`. Getting one level wrong is the single most common mistake with this function. ## The rules on target The target must be: * **a pointer**, and * **not nil**, and * pointing at either a **type that implements `error`**, or at **any interface type**. Violate any of those and `errors.As` panics. The panics are distinct — a nil target, a non-pointer target, and a pointer to something that is neither an interface nor an error — but they all mean the same thing: the call as written can never work for any input. ## Why a panic rather than false Returning `false` would be worse. `false` is a legitimate answer that the calling code will act on, so a permanently-wrong target would silently disable a branch: retries that never happen, an HTTP 500 where a 422 belonged, a validation failure that never gets reported per-field. A panic makes the mistake impossible to miss the first time the code path runs. It is treated as a bug in the program, not as a runtime condition. ## The trap: it only fires on the error path That design has one sharp edge. `errors.As` is called precisely when something has gone wrong, so a bad target sits dormant through every test that exercises the success path. A package generated from a schema is a good example: the generator emits both the error type and the handling code, and if the emitted call passes a value rather than a pointer, the whole package tests green and then panics in production the first time a document fails validation — turning a routine 422 into a crashed request, or a dead process if nothing recovers. Two defences: * `go vet`'s **`errorsas`** analyzer reports a call to `errors.As` whose second argument is not a pointer to a type implementing `error`. It is part of the vet subset that `go test` runs by default, so a plain `go test ./...` over the generated package catches it without extra wiring. * A test that actually drives one failing input through the public function, so the error path is executed at least once. ## Interface targets A pointer to an *interface* type is allowed and is genuinely useful when you care about a capability rather than a named type. If a third-party SDK's error types are not exported, but they all carry a method you can name, you can match on it: ```go var coded interface{ StatusCode() int } if errors.As(err, &coded) { log.Printf("upstream status %d", coded.StatusCode()) } ``` Here `*target` is an interface type, so the rule about implementing `error` does not apply, and the first error in the chain that has the method wins. ## Several matches If more than one error in the chain matches, you get the **first one found** in traversal order — the outermost — and the search stops there. If you need a specific one of several, match on a narrower type. ## Versions `errors.As` arrived in Go 1.13 together with `errors.Is`, `errors.Unwrap` and the `%w` verb. Go 1.20 extended the traversal to error trees. Go 1.26 added `errors.AsType`, a generic alternative that returns the extracted value and a bool instead of taking a pointer target, which removes this whole class of target mistakes at the cost of requiring a newer toolchain.

  • What does errors.As do when several errors in the chain match the target type?
    It stops at the first match in traversal order — the outermost one — assigns it to the target and returns true. Inner errors of the same type are never seen. If you need a specific one, match on a narrower type or unwrap deliberately rather than relying on the search order.
  • Can the target point at an interface type instead of a concrete type?
    Yes, and that is the escape hatch when a dependency does not export its error types. Declare a variable of an interface naming the method you care about, such as `interface{ StatusCode() int }`, and pass its address; the first error in the chain implementing it is assigned. Pointers to interface types are explicitly allowed by the target rules.
  • How would you stop a bad errors.As target from reaching production?
    Run `go vet`, whose `errorsas` analyzer reports the call at build time and is part of the subset `go test` runs by default, so `go test ./...` catches it. Back that with at least one test that drives a real failure through the public API, since the panic only fires on the error path.

The target is an out-parameter: you hand errors.As an empty box to drop the match into. Give it a copy of the box instead of the box's address and there is nowhere for the result to go — which is why it refuses loudly rather than pretending it found nothing.

saying these in an interview costs you the question

  • Says a bad target makes errors.As return false
  • Passes the error variable itself as the target
  • Declares var fe FieldError and passes &fe when the package returns *FieldError
  • Thinks errors.As only inspects the outermost error
  • Believes a pointer to an interface type is illegal as a target