skip to content

error as a Value

error is an ordinary interface value returned beside the result, which is why the famous trap bites: a nil *MyError stored in an error is not a nil error.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

3

What does Go's built-in error interface require a type to implement?

level: juniorimportance: must knowfreq 85%

answer

  1. one method, nothing else
  2. it returns a printable string
  3. satisfaction is never declared
  4. the method name matches the type name
  5. structs are not required

basics

~20 s

error is a predeclared interface with exactly one method, Error() string. Any type that declares that method - a struct, a named int, anything - is an error and can be returned wherever error is expected.

solid answer

~50 s

`error` is not a class or a keyword: it is a predeclared interface type whose entire definition is `type error interface { Error() string }`. Satisfaction is implicit, so a type becomes an error simply by declaring a method named `Error` that takes nothing and returns a `string` - there is no `implements` clause and nothing to register. That means any named type works, not just structs; a `type exitCode int` with an `Error() string` method is a perfectly good error. Because `error` is an interface value, its zero value is `nil`, which is how a function signals success. `fmt` verbs like `%v` and `%s` call `Error()` for you when printing, and the standard library ships plenty of concrete implementations such as `*fs.PathError`, `*net.OpError` and `*strconv.NumError`, alongside the anonymous ones `errors.New` and `fmt.Errorf` hand back.

code

go · 10 lines
go
type ValidationError struct {
	Field string
	Msg   string
}

func (e *ValidationError) Error() string {
	return e.Field + ": " + e.Msg
}

// *ValidationError may now be returned wherever an error is expected.

go deeper

for a junior

Be ready to write the interface from memory: one method, Error() string, no arguments. Say that satisfaction is implicit and that nil is the zero value meaning success.

for a middle

Explain that the method signature must match exactly, that any named type can carry it, and that fmt calls Error() for %v - preferring it over String() when both exist.

for a senior

Show design judgment: when a plain message type is enough versus a concrete type worth exporting, and why an interface this small keeps wrapping and classification as library choices rather than language rules.

for a principal

Frame it as an API-surface decision. The one-method contract means every richer behaviour your teams rely on is a convention you must choose, document and hold stable across packages.

## The whole contract Go has no exception class, no `Throwable` root type and no `throws` clause. A failing operation reports failure by returning a value, and the type of that value is `error` - a *predeclared* interface, available without importing anything, whose definition is just: ```go type error interface { Error() string } ``` That is the entire contract. One method, no arguments, one `string` result. Everything else people associate with Go errors - sentinel values, wrapping, typed errors carrying fields - is built *on top of* this one-method interface by ordinary library code, not by the language. ## Satisfaction is implicit Go interfaces are satisfied structurally. You never write that a type implements `error`; the compiler checks, at every assignment or call, that the type has the required method. So this type is an error the moment the method exists: ```go type ValidationError struct { Field string Msg string } func (e *ValidationError) Error() string { return e.Field + ": " + e.Msg } ``` Nothing in that snippet mentions `error` at all. `*ValidationError` may now be returned from any function declared to return `error`, stored in an `error` variable, put in a `[]error`, sent over a `chan error` or kept in a struct field. The method signature must match *exactly*. A method named `error` (lowercase) does not count - it is a different name, and it is also unexported. `Error() (string, error)` does not count. `Error(prefix string) string` does not count. If your type "mysteriously" will not compile as an error, compare the signature character by character first. The receiver you declare decides which of `T` and `*T` carries the method, which matters when you design a custom error type; that receiver choice is a topic of its own. For the purposes of "what makes something an error", the point is only that some type ends up with the method. ## Any type, not just structs Because the requirement is a method and Go lets you declare methods on any named type in your package, an error does not have to be a struct: ```go type exitCode int func (c exitCode) Error() string { return "exit status " + strconv.Itoa(int(c)) } ``` A named string type, a named slice type, even a named function type can be an error. This is a real design tool: when the failure is one small value, a named scalar is often a better error than a one-field struct. ## The zero value is the success signal `error` is an interface value, and the zero value of any interface is `nil`. That is why a function returning `error` costs nothing extra on the happy path: it returns `nil`, and the caller's check sees an interface with nothing in it. A `nil` error is not a special "no error" object; it is genuinely the absence of a value. One consequence to be aware of early: because an interface value carries both a type and a value, an interface that has been given a *concrete type* but a nil value is **not** `nil`. That is the classic trap when a helper is declared to return a concrete pointer type instead of `error`, and it is worth knowing exists the moment you learn what the interface is. ## How errors get printed You almost never call `Error()` yourself. `fmt` does it: for `%v`, `%s` and friends, if an operand implements `error`, `fmt` calls `Error()` and prints the result. That check takes precedence over `fmt.Stringer`, so a type declaring both `Error() string` and `String() string` will print via `Error()`. A `nil` error printed with `fmt.Println` shows `<nil>`. ## Where the implementations come from Most errors you handle are one of three shapes: - The anonymous ones from `errors.New` and `fmt.Errorf`, which return values of small unexported types whose only job is to hold a message string. - Concrete types exported by the standard library so callers can inspect them, such as `*fs.PathError` (with `Op`, `Path` and `Err` fields), `*net.OpError` and `*strconv.NumError`. - Your own types, declared exactly like `ValidationError` above. All three are the same thing to the caller: a value satisfying a one-method interface. ## Two things that follow First, satisfaction is not opt-in, so it can happen by accident. If some unrelated type gains a method called `Error` that returns a `string`, that type is now assignable to `error`, and a careless `return x` may compile where you did not intend it to. Second, `error` carries nothing but a message unless you add more. No stack trace, no error code, no cause - those come from choosing a richer concrete type or from wrapping. The interface deliberately asks for the minimum so that everything else stays a library decision.

  • Is error a keyword, or a type declared somewhere you can read?
    It is a predeclared identifier in the universe block - an ordinary interface type available without an import, spelled `type error interface { Error() string }`. Nothing about it is compiler magic. `errors.New` and `fmt.Errorf` are equally ordinary: plain library functions that return values of small types implementing that method.
  • If a type declares both Error() string and String() string, which one does fmt use for %v?
    `Error()`. When `fmt` formats an operand with `%v`, `%s` and similar verbs, it checks for the `error` interface before `fmt.Stringer`, so `Error()` wins. Declaring both is legal but usually a smell - pick one representation so logs and error messages cannot drift apart.
  • Can a type satisfy error by accident?
    Yes. There is no opt-in: any type that happens to declare `Error() string` is assignable to `error`, so a method added for an unrelated reason can silently make a type usable as an error and let a mistaken `return x` compile. It is a small risk, but it is why the method name is worth reserving for real errors.

The error interface is a job description with a single duty: say in words what went wrong. Any type that can do that is hired, without applying.

saying these in an interview costs you the question

  • Says error is a base struct you embed
  • Thinks a type must declare that it implements error
  • Names the method error() or Message() instead of Error()
  • Believes only structs can be errors
  • Claims returning an error unwinds the stack
open as a page

A helper returns a nil *ValidationError as an error and the caller's check fires - what is the fix?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Declare the helper's result type as error, not *ValidationError, and return a literal nil on success. A concrete nil pointer stored in an error is an interface that carries a type, so it never compares equal to nil and every successful call looks like a failure.

open as a page

When do you use errors.New versus fmt.Errorf to construct an error in Go?

level: middleimportance: should knowfreq 65%

basics

~20 s

Use errors.New when the message is fixed text. Use fmt.Errorf when the message must interpolate runtime values, since it takes a format string and arguments. Both return a value satisfying the error interface, and neither is special to the compiler.

open as a page