Error Handling Basics — Specification¶
Table of Contents¶
- Introduction
- The Predeclared error Interface
- Spec Text on Error
- Multi-Value Returns: Spec Mechanics
- Nil Interface Values
- The errors Package: Public API
- Compatibility and Versioning
- Idioms Codified by the Spec or Standard Library
- Comparing the Spec to Real Code
- Differences Across Go Versions
- Things the Spec Does NOT Say
- References
Introduction¶
The Go specification defines the language. The errors package and idioms are convention layered on top of that small core. This file separates "what the spec actually says" from "what the community has agreed to do."
Reference: The Go Programming Language Specification.
The Predeclared error Interface¶
From the Predeclared identifiers section of the spec:
Types: any, bool, byte, comparable, complex64, complex128, error, float32, float64, int, int8, int16, int32, int64, rune, string, uint, uint8, uint16, uint32, uint64, uintptr
error is a predeclared interface type. The spec defines it as if written:
It lives in the universe block (the outermost scope), so you can use it anywhere without importing.
Predeclared status means: - You cannot redefine it at package scope (no type error int). - You can shadow it inside a smaller scope (a local variable named error), but that is universally a bad idea. - It is part of the language, not the standard library — even a program with no imports can use it.
Spec Text on Error¶
The spec mentions error in only a few places:
- Predeclared identifiers — listed as a type.
- Type assertions —
e.(error)is valid for asserting an interface holds an error. - Type switches —
switch e := x.(type) { case error: ... }.
That is essentially all the spec says about error. The semantics — when to use it, how to wrap it, how to compare it — are convention, not spec.
The spec also defines panic and recover as built-in functions, and these interact with errors at runtime, but the error type itself is not coupled to panic.
Multi-Value Returns: Spec Mechanics¶
The error idiom relies on multi-valued returns. From the spec, Function types:
A function may return multiple values. The return statement may include a list of expressions whose number and types match the function's result list.
FunctionType = "func" Signature .
Signature = Parameters [ Result ] .
Result = Parameters | Type .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
A signature func() (int, error) is two unnamed return parameters. The compiler enforces that all paths return both values.
From Assignments:
The assignment proceeds in two phases. First, the operands of index expressions and pointer indirections [...] on the left and the expressions on the right are all evaluated. Second, the assignments are carried out in left-to-right order.
So n, err := f() evaluates f(), then assigns both returns simultaneously. n and err are guaranteed to be a consistent pair from the same call.
Nil Interface Values¶
From the spec, Interface types:
The value of an uninitialized interface is nil.
Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.
So var err error is nil (both type and value words are zero). err == nil is true.
The famous trap:
type MyErr struct{}
func (*MyErr) Error() string { return "x" }
func f() error {
var p *MyErr = nil
return p // returns *non-nil* interface!
}
This is specified behavior, not a bug. The interface value has dynamic type *MyErr (non-nil type word) and dynamic value nil (nil data word). Per the spec, equality requires both to be nil, so the interface is non-nil.
The fix: return an explicit nil from the function when there is no error, do not pass through a typed nil pointer.
The errors Package: Public API¶
Defined in $GOROOT/src/errors/. Public surface:
// errors.go
func New(text string) error
// wrap.go (Go 1.13+)
func Unwrap(err error) error
func Is(err, target error) bool
func As(err error, target any) bool
// join.go (Go 1.20+)
func Join(errs ...error) error
Method conventions for custom errors:
type MyErr struct{ /* fields */ }
func (e *MyErr) Error() string { /* required */ }
func (e *MyErr) Unwrap() error { /* optional, for wrapping */ }
func (e *MyErr) Is(target error) bool { /* optional, custom Is */ }
func (e *MyErr) As(target any) bool { /* optional, custom As */ }
errors.Is and errors.As use Unwrap recursively. They also call the optional Is / As methods if defined.
Compatibility and Versioning¶
Go promises strict backward compatibility within 1.x. Errors-related additions:
| Go version | Addition |
|---|---|
| 1.0 | errors.New, the error interface |
| 1.13 | errors.Is, errors.As, errors.Unwrap, fmt.Errorf %w verb |
| 1.20 | errors.Join, multiple %w verbs in fmt.Errorf |
| (future) | Ongoing community discussion of stack-trace integration |
Old code using only errors.New and == continues to compile and run unchanged. The newer features are additive.
Idioms Codified by the Spec or Standard Library¶
Some "idioms" are required by tooling or stdlib behavior:
- Error message starts lowercase, no trailing punctuation. Codified by
golintand the standard library. Reason: errors compose asfmt.Errorf("op: %w", err)and double-capitalization looks silly. erroris the last return value. Tooling likeerrcheckassumes this.fmt.Errorfwith%wfor wrapping. Defined in stdlib;%vdoes not wrap.Is/As/Unwrapmethod names. Used byerrors.Is/errors.As. If you misspell, the chain breaks silently.
Comparing the Spec to Real Code¶
The spec is minimal: an interface and a few rules about returning values. Real code piles convention on top:
| Spec | Convention |
|---|---|
error is an interface | Use as last return value |
| Returning nil interface = no error | Always check err != nil |
| No language wrapping | Use fmt.Errorf("%w", ...) |
| No comparison rules | Use errors.Is, not ==, for wrapped errors |
A rookie reading only the spec will know how to declare an error but not how to use one well. Conversely, a developer who learns only the conventions sometimes misunderstands edge cases like the typed-nil gotcha. Both layers matter.
Differences Across Go Versions¶
Behavior worth knowing version-by-version:
- Pre-1.13:
errors.Unwrap,errors.Is,errors.Asdid not exist. Wrapping was done via third-party packages likegithub.com/pkg/errors. - 1.13: Standardized wrapping via
%wand theerrorspackage functions. Old code still works; new code can adopt them. - 1.20:
errors.Joinfor combining; multiple%winfmt.Errorf. - Future: There has been discussion (and rejected proposals) of
trykeyword and stack traces. Status: not landed.
If you maintain code that supports Go versions older than 1.13, you cannot use %w. For modern code (1.21+), use the full feature set.
Things the Spec Does NOT Say¶
- The spec does not require you to check errors.
f()(with no assignment) whenfreturns(T, error)is legal and silently discards both returns. - The spec does not require error messages to start lowercase. That is convention.
- The spec does not require
%wformatting. Convention. - The spec does not require
Unwrap/Is/Asmethods. They are an opt-in protocol. - The spec does not link errors to panic. They are independent mechanisms.
This is by design. The spec keeps the language small; the convention layer keeps the ecosystem coherent.
References¶
- The Go Programming Language Specification — Predeclared identifiers
- The Go Programming Language Specification — Function types
- The Go Programming Language Specification — Interface types
- Package errors documentation
- Go 1.13 release notes — error wrapping
- Go 1.20 release notes — errors.Join
$GOROOT/src/errors/errors.go$GOROOT/src/fmt/errors.go