---
title: 'The Error Interface'
source: 'https://academia.sh/en/courses/go-type-system/error-interface'
course: 'Interfaces, Errors and Generics'
language: en
updated: '2026-09-14T10:59:33+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that three separate types implicitly satisfy the error interface and that their dynamic types are read with %T'
    - 'Distinguishes by running it the difference in comparison between a value-receiver and a pointer-receiver error'
    - 'Shows by measurement why the interface is not nil when a nil pointer is placed into the error interface'
---

# The Error Interface

An error is an ordinary value in Go and implicitly satisfies the error interface: any type that writes Error() string becomes an error without ever saying so in its source. Three types confirm this, their dynamic types read with %T; a value-receiver error's structural equality is shown apart from a pointer-receiver error's identity equality. The limiting measure: when a nil pointer is placed into the error interface, the interface itself is not nil.

The previous topic's last lesson measured that a consumer can define an interface the
producing side never writes in its source at all — a natural consequence of the binding
never being written in the source. This lesson looks at where that same model is used most in
Go: the standard library itself is built around a single interface, `error`, and every error
value satisfies it in exactly the same way, without writing any declaration.

`error` is an ordinary return value in Go and an ordinary interface — it has a single method:
`Error() string`. This lesson's question is the same one carried over from the previous
topic: where is the binding that satisfies this interface established? To find the answer,
three separate types will be returned as errors, with the word `error` never appearing in any
of their source.

The Functions lesson (M09/K01) already measured `error` as an ordinary return value and
showed that a caller ignoring it without ever catching it is rejected in no way — that
observation is not rebuilt here. This lesson looks at **what** that return value is, and when
a type counts as "an error." The question asked will stay the same for the rest of the
course: is a binding written explicitly in the source, established at the use site the
compiler looks at, or carried only inside the value at run time?

This topic advances this question in four steps. This lesson measures why an error is a
value and how it satisfies the interface; the next lesson measures where an error's identity
stands, the third measures how context added to an error is preserved through a chain, and the
fourth measures where the binding of an exit path never visible in the signature — panic — is
established (or not).

## Three Types, Zero Declarations

The previous topic measured implicit satisfaction on a single type in the Interface Concept
lesson; this lesson applies the same mechanism to error values. The three types below were
written independently of each other: one a value-receiver struct, one a pointer-receiver
struct, one a named type built on an integer. What all three have in common is a single line:
a method matching the `Error() string` signature.

Choosing three types in deliberately different forms is not arbitrary. The `error` interface
is used so widely in the standard library that almost every type shape — value, pointer,
named base type, even an empty struct — becomes able to satisfy it. The measurement's goal is
to show that this diversity does not affect satisfaction at all: the interface wants a single
method, and does not care which type carries that method or how.

```go
// binding.go — is the error binding established at the use site, or inside the value
package main

import "fmt"

var useSite, value int

func observe(name, location, evidence string) {
	switch location {
	case "use":
		useSite++
	default:
		value++
	}
	fmt.Printf("%-46s %-9s %s\n", name, location, evidence)
}

func summary() {
	fmt.Printf("binding: %d use site, %d value\n", useSite, value)
}
```

```go
// measurement.go — an error is a value: three types write Error() string without ever mentioning it, and become errors
package main

import "fmt"

type FormatError struct{ Field string }

func (b FormatError) Error() string { return "invalid format: " + b.Field }

type AccessError struct{ Code int }

func (e *AccessError) Error() string { return fmt.Sprintf("access denied: code %d", e.Code) }

type CounterError int

func (s CounterError) Error() string { return fmt.Sprintf("counter overflow: %d", int(s)) }

func main() {
	var h1 error = FormatError{Field: "name"}
	var h2 error = &AccessError{Code: 403}
	var h3 error = CounterError(9)

	observe("three types satisfying the error interface", "use",
		"the word 'error' never appears in their source")
	observe("error value's dynamic type", "value", fmt.Sprintf("%T, %T, %T", h1, h2, h3))

	var a error = FormatError{Field: "name"}
	var b error = FormatError{Field: "name"}
	observe("equality of two value-receiver errors", "value", fmt.Sprint(a == b))

	var c error = &AccessError{Code: 403}
	var d error = &AccessError{Code: 403}
	observe("equality of two pointer-receiver errors", "value", fmt.Sprint(c == d))

	summary()
}
```

```
three types satisfying the error interface     use       the word 'error' never appears in their source
error value's dynamic type                     value     main.FormatError, *main.AccessError, main.CounterError
equality of two value-receiver errors          value     true
equality of two pointer-receiver errors        value     false
binding: 1 use site, 3 value
```

**EH1.** None of the three types ever mentions the word `error` in its source; `FormatError`,
`AccessError` and `CounterError` only write a method matching the `Error() string` signature,
and this is enough to make them errors. The binding is established at the line
`var h1 error = FormatError{...}`, the moment the value is **placed** into an `error`
variable — nowhere in the source does it say "this type satisfies the error interface." The
dynamic type read with `%T` confirms that all three actually complete satisfaction: the value
type appears as `main.FormatError`, the pointer type as `*main.AccessError`, the named type as
`main.CounterError`.

**EH2.** The last two lines produce and compare two independent copies from the same
`FormatError` value, and two independent pointers from the same `AccessError` value. A
value-receiver error is compared **structurally**: two separate variables come out equal
because their fields are the same. A pointer-receiver error, on the other hand, is compared by
**identity**: two separate `&AccessError{...}` calls produce two separate addresses, and
equality returns `false`, even when their fields are exactly the same. This difference is not
arbitrary — the next lesson builds a package-level error value's identity exactly on this
distinction.

These two lines also carry a small warning: the `error` interface supporting the comparison
operator (`==`) does not mean two errors are "the same error" — it only means the pair the
interface carries (dynamic type, dynamic value) is equal on both sides. For a value-receiver
error, this reduces to the equality of the fields; if even one field differs, the comparison
returns `false`. This distinction joins, in the section below, the question of which type's
method set the same method enters.

## Method Set: It Is the Type That Satisfies, Not the Value

The Go Fundamentals course's Pointers lesson measured that whether a method has a value
receiver or a pointer receiver comes from the receiver's definition, not from the call
notation — that mechanic is not rebuilt here. But for this lesson's question, one single
consequence matters: when `AccessError`'s `Error` method is written with a pointer receiver,
which type actually satisfies the `error` interface — `AccessError` itself, or only
`*AccessError`? Asking the compiler for the answer requires borrowing K01's `compiles` tool: a
piece of source is written to a temporary package, compiled, and only whether it passed is
read.

This tool only reads whether it passed, it never reads the error text — what is measured is
not "why was it rejected," it is "was it rejected." Both pieces of source share the same
`AccessError` definition and the same empty function body; the only difference between them is
whether the value placed into the `error` variable carries a `.` mark. This also shows how
small a distinction interface satisfaction can rest on, at the level of syntax.

```go
// methodset.go — a pointer-receiver method only puts the pointer type into the error interface
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

const types = `
package trial
type AccessError struct{ Code int }
func (e *AccessError) Error() string { return "access denied" }
`

func main() {
	valueAssignment := types + "func F() { var _ error = AccessError{Code: 1} }"
	pointerAssignment := types + "func F() { var _ error = &AccessError{Code: 1} }"

	observe("assigning a value type to an error variable", "use", fmt.Sprint(compiles(valueAssignment)))
	observe("assigning a pointer type to an error variable", "use", fmt.Sprint(compiles(pointerAssignment)))
	summary()
}
```

```
assigning a value type to an error variable    use       false
assigning a pointer type to an error variable  use       true
binding: 2 use site, 0 value
```

**EH3.** The `AccessError` value stops compilation when it tries to be assigned to `error`;
the `*AccessError` pointer passes the same line without a problem. Both carry the same
fields, both can have the same `Code` value — the distinction is not in the data itself, it is
in which type has the `Error() string` method. When a method is written with a pointer
receiver, that method enters only the pointer type's **method set**; the value type's method
set never contains it.

**EH4.** This clarifies even further where implicit satisfaction is established: the binding
looks not only at "is there an `Error() string` method," but at "does this **exact type's**
method set have this method." The compiler checks this at every `var _ error = ...` line, at
the **use site** where the assignment is made — even though a declaration like
`AccessError implements error` never appears anywhere in the source, the check is real and
produces two different results.

This distinction has a practical consequence: writing a type's error method with a pointer
receiver means only that type's pointer form will count as an error. If a function tries to
return the value `AccessError{...}` directly as `error`, compilation stops right there; the
function needs to return `&AccessError{...}`. This choice is not arbitrary — a design that
wants to prevent a large struct from being copied on every return has, at the same time, also
chosen that only that struct's pointer form enters the error interface.

## Limiting Measure: A Full Interface Does Not Look nil

What a path that does not return an error returns is also an `error` value — usually `nil`.
But where this `nil` comes from matters. The `validate` function below returns a concrete
pointer type (`*FieldError`), and when the field is filled in, this pointer really is `nil`.
The problem starts on the line where this concrete value is placed into an `error`
variable — and this line looks as ordinary as the `check` function's return statement.

This example combines two observations measured in the previous two sections into a single
line: `FieldError`'s `Error` method has a pointer receiver, so only `*FieldError` satisfies
`error` — just like `AccessError`. And an interface value is a pair carrying the dynamic type
and the dynamic value together — just like the three types measured in the first section. The
difference this time is that the dynamic value itself is `nil`, and this does not affect the
interface as a whole.

```go
// trap.go — a nil-pointer concrete error is no longer nil once placed into the error interface
package main

import "fmt"

type FieldError struct{ Field string }

func (a *FieldError) Error() string { return "invalid field: " + a.Field }

func validate(field string) *FieldError {
	if field == "" {
		return &FieldError{Field: "required"}
	}
	return nil
}

func check(field string) error {
	return validate(field)
}

func main() {
	concrete := validate("name")
	err := check("name")

	observe("the concrete pointer itself", "value", fmt.Sprint(concrete == nil))
	observe("error interface carrying the same value", "value", fmt.Sprint(err == nil))
	observe("error interface's dynamic type", "value", fmt.Sprintf("%T", err))
	summary()
}
```

```
the concrete pointer itself                    value     true
error interface carrying the same value        value     false
error interface's dynamic type                 value     *main.FieldError
binding: 0 use site, 3 value
```

**EH5.** The call `validate("name")` really returns a `nil` pointer — the third line confirms
this. But the moment `check` **returns** this pointer in a variable of type `error`, the
interface value becomes two-part: the dynamic type (`*FieldError`) and the dynamic value
(`nil` pointer). The comparison `err == nil` looks at whether the interface **itself** is
empty; because the dynamic type is filled in, the interface is counted as full too, even
though the pointer it carries is `nil`.

**EH6.** This is the topic's common measure's strangest counterpart among error values: eight
of the sixteen bindings were carried inside the value, and this line shows exactly why those
eight are not free. The place the binding breaks is neither compilation nor the moment of the
call — it surfaces only when `== nil` is asked of the value. The practical consequence is
clear: if a function is going to return `error`, its signature should use the `error`
interface directly, not a concrete pointer type; otherwise, a `nil` return from a function
like `validate` can turn into a full error in the caller's hands.

The insidiousness of this trap is not in the error staying **hidden**, it is quite the
opposite: the check `err != nil` works perfectly honestly and gives the correct answer — the
interface really is full, because it carries the dynamic type. What is mistaken is the
expectation of the person who wrote the code: assuming that in every case `validate` returns
`nil`, `check` will also return `nil`. This assumption comes from thinking that the pointer
itself and the interface carrying the pointer are the same thing; but as the third line
shows, the two answer different questions.

The four observations measured throughout this lesson settle into a single frame. The
implicit satisfaction binding is never written in the source, and two different mechanisms
fill its place: the method-set match the compiler checks at an assignment line (use site), and
the dynamic type and dynamic value the interface value carries within itself (value). These
two are not independent of each other — the binding established at the use site determines
**what** the pair later carried inside the value will carry. The `nil` trap is born exactly in
this transition: once the match is established, the interface is full from then on, even if
the carried value itself is `nil`.

## Summary

- `error` is an ordinary single-method interface (`Error() string`); a type satisfying it is
  written nowhere in the source, it is established only by the method's presence.
- A value-receiver error is compared structurally (the value is equal if the fields are
  equal); a pointer-receiver error is compared by identity (the value is not equal if the
  addresses are not equal).
- The dynamic type read with `%T` shows which concrete type an error value satisfies the
  interface with; this information stands not in the source, but in the value itself.
- Even when a concrete pointer type is `nil`, once that pointer is placed into an `error`
  variable, the interface value is counted as full — the comparison returns `false` because
  the dynamic type is carried.
- This behavior is the reason functions that will return `error` should use the `error`
  interface directly in their signature, not a concrete type.
- The binding is established in two places, in neither as a declaration: method-set matching
  is the use site the compiler checks at an assignment line, and the pair of dynamic type and
  dynamic value is the value itself.

## Next Step

The two errors measured in this lesson (`FormatError`, `AccessError`) were each written
individually and reproduced on every call; neither was shared as a single package-level value.
But when a caller wants to **recognize** a specific error type — wants to ask "is this exactly
that error?" — it needs to rely on the error's identity, not its text. The comparison
difference between value-receiver and pointer-receiver errors measured in this lesson
prepares exactly the ground for this question: making an identity reliable requires the
comparison to be identity-based, not structural. Just as this lesson measured that the type
satisfying the `error` interface never mentions the word `error`, the next lesson measures
where this identity stands: why can an error value defined once at the package level still be
told apart from another error carrying the same text?
