skip to content

ErrNotFound or NotFoundError: how do you name errors in a Go package other teams import, and what does exporting one commit you to?

level: seniorimportance: should knowfreq 40%

answer

  1. prefix a value, suffix a type
  2. one is compared, the other unpacked
  3. io.EOF predates the rule
  4. an exported sentinel is a promise
  5. breaking it fails silently, not loudly

basics

~20 s

Go prefixes an error value with Err and suffixes an error type with Error: ErrNotFound is a sentinel, NotFoundError a struct. Exporting either makes it API, because callers branch on it and you cannot then stop returning it.

solid answer

~50 s

The convention splits on what the thing is. A sentinel error *value* gets the `Err` prefix — `sql.ErrNoRows`, `os.ErrNotExist`, `fs.ErrPermission` — and an error *type* gets the `Error` suffix, as in `fs.PathError`, `net.OpError`, `json.SyntaxError`. That single letter of difference tells a caller which tool to reach for: a sentinel is compared with `errors.Is`, a type is extracted with `errors.As` so its fields can be read. A few standard-library names predate the convention and are worth knowing as exceptions — `io.EOF`, `context.Canceled`, `context.DeadlineExceeded`. The commitment matters more than the spelling: an exported sentinel is a promise that this exact value keeps coming back for this condition, and callers will encode that promise in their control flow. If a condition is not something callers should branch on, keep the error unexported or unwrapped rather than publishing a name you cannot retract.

code

go · 13 lines
go
var ErrNotFound = errors.New("store: not found")

type ValidationError struct {
	Field string
}

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

func load(id string) error {
	return fmt.Errorf("load %s: %w", id, ErrNotFound)
}

go deeper

for a junior

Learn the two shapes: a package-level value named ErrSomething, and a type named SomethingError. Recognise sql.ErrNoRows and os.ErrNotExist as the standard examples.

for a middle

Explain what each shape is for: a sentinel is compared, a type is unpacked for its fields, and the naming tells a caller which. Know io.EOF as the historical exception.

for a senior

Treat an exported sentinel as a contract. Say what breaks when a layer stops wrapping with %w, why that failure is silent, and how you decide which conditions deserve to be branchable at all.

for a principal

Own the size of the error surface. Every exported sentinel is a case downstream teams encode in their control flow and you can never retract; decide deliberately how many exist, document which functions return them, and keep the rest unexported.

## The two naming rules Go distinguishes an error *value* from an error *type*, and names them differently on purpose. **Sentinel values take the `Err` prefix.** These are package-level variables, conventionally created with `errors.New`, that represent one specific condition: ``` var ErrNotFound = errors.New("store: not found") ``` The standard library is full of them: `sql.ErrNoRows`, `os.ErrNotExist`, `os.ErrPermission`, `fs.ErrPermission`, `bufio.ErrBufferFull`. **Error types take the `Error` suffix.** These are types, usually structs, that carry data about the failure: `fs.PathError` (which path, which operation), `net.OpError` (which network, which address), `json.SyntaxError` (which byte offset), `strconv.NumError`, `url.Error`, `time.ParseError`. So `ErrNotFound` and `NotFoundError` are not two spellings of the same idea. They are a variable and a type, and the naming tells them apart before you have opened the file. ## Why the distinction earns its keep The two shapes are consumed by two different functions, and the name tells the caller which one to write. - A sentinel is matched with `errors.Is(err, pkg.ErrNotFound)`, which walks the wrapping chain looking for that exact value. - A type is extracted with `errors.As(err, &pathErr)`, which finds the first error in the chain assignable to the target and gives you access to its fields. A caller who sees `ErrNotFound` in your documentation knows immediately that there is nothing to unpack — the condition is the whole message. A caller who sees `NotFoundError` knows there are fields worth reading. Naming them the same way would erase that. The corollary is a design question worth stating: choose a **sentinel** when the condition is a fact with no payload, and a **type** when the caller genuinely needs details. Publishing a type nobody destructures is surface you must keep compiling; publishing a sentinel where the caller needs the offending key means every caller reconstructs context you already had. ## The exceptions worth knowing The convention was formalised after some of the standard library was written, so a handful of famous errors do not follow it. `io.EOF` is the one everybody names. `context.Canceled` and `context.DeadlineExceeded` are two more. These are not evidence that the rule is soft — they are historical, and new code does not imitate them. Being able to name them is a cheap way to show you have read the standard library rather than a style summary. ## What exporting commits you to This is the part that separates a naming answer from a senior one. An exported sentinel is not documentation, it is **behaviour other people branch on**. Once `store.ErrNotFound` exists and is returned from `Get`, callers write: ``` if errors.Is(err, store.ErrNotFound) { // create it instead } ``` That code is now coupled to the promise that this condition keeps producing this value. Three practical consequences follow. **You cannot stop returning it.** Replacing the sentinel with a richer error type, or letting the underlying driver's error escape instead, silently breaks every caller's branch — silently, because their `errors.Is` simply stops matching and their fallback path runs. No compiler error, no test failure in your repo. **You must keep wrapping it.** If you wrap errors as they cross layers with `fmt.Errorf("...: %w", err)`, the sentinel stays reachable through `errors.Is`. If a layer reformats with `%v` or builds a fresh error from the message, the chain is cut and the caller's branch dies just as quietly. **You are choosing which conditions are branchable.** Every exported sentinel says "this is a case you are expected to handle". A package with fifteen of them is asking callers to write a switch; a package with one is saying most failures are simply failures. That is an API design call, and it deserves as much thought as the function signatures. The practical rule: export the errors callers must be able to *act on differently*, keep the rest unexported or unwrapped, and document exactly which functions can return which sentinel — because if it is not documented, it is not really a contract, it is an accident callers will depend on anyway. ## Local shape rules that come up in review - Sentinels are conventionally `var`, not `const` — `errors.New` returns an interface value, which cannot be constant. - The message inside is lowercase and unpunctuated, so it composes when wrapped: `errors.New("not found")`, not `errors.New("Not found.")`. - The `Err` prefix belongs to *error* values specifically; do not use it for a struct field or a function that merely deals with errors. - An unexported sentinel is spelled the same way with a lowercase first letter: `errNotFound`. That is the right default while you are still deciding whether the condition is part of your contract.

  • How does the naming tell a caller whether to use errors.Is or errors.As?
    An `Err`-prefixed name is a value, so callers compare it with `errors.Is(err, pkg.ErrNotFound)`. An `Error`-suffixed name is a type carrying fields, so callers extract it with `errors.As(err, &target)` and read those fields. The prefix-versus-suffix difference is the fastest signal a package gives about which shape it returns.
  • Which standard-library errors break the Err prefix convention?
    `io.EOF` is the famous one, and `context.Canceled` and `context.DeadlineExceeded` are two more. They predate the convention being written down and were never renamed because their names are load-bearing across the ecosystem. New code follows the `Err` prefix rather than imitating them.
  • What breaks a caller's errors.Is check even though your sentinel still exists?
    A layer that reformats the error instead of wrapping it. `fmt.Errorf("load: %v", err)` produces a new error with no link back, so the chain is cut and `errors.Is` stops matching. Using `%w` preserves it. The failure is silent — the caller's branch simply never runs, and nothing in your own tests notices.
  • When would you deliberately not export a sentinel?
    When callers have no different action to take. An exported sentinel invites branching and then freezes that condition into your API; if every caller would just log and return, an unexported `errNotFound` keeps the message useful without the contract. Export the errors that change what the caller does, and document precisely which functions return them.

saying these in an interview costs you the question

  • Names a sentinel value NotFoundError and a type ErrNotFound
  • Treats the Err prefix as decoration with no consequence
  • Exports a sentinel for every internal failure condition
  • Reformats wrapped errors with %v and expects errors.Is to match
  • Writes uppercase punctuated error messages that read badly when wrapped