AlgoMaster Logo

The error Interface

Last Updated: May 22, 2026

High Priority
12 min read

Go doesn't have exceptions. Instead, functions that can fail return an error value alongside their normal result, and the caller checks it explicitly. The whole mechanism rests on a single tiny interface built into the language: error. This lesson covers what that interface looks like, why it's an interface at all, the if err != nil check, the convention of returning error last, how standard library functions hand errors back, and a first look at what it takes to make your own type satisfy error.

What error Actually Is

The error type in Go is a builtin interface with exactly one method:

Anything that has an Error() string method satisfies this interface. That's the contract. No base class, no special exception hierarchy, no magic. An error is any value whose type knows how to describe itself as a string.

Because error is built into the language, you don't import anything to use it. It lives in the same universe block as int, string, bool, and nil. You can write func ... (err error) in any file, in any package, without an import.

A freshly declared error variable is nil, the same way a freshly declared pointer or interface variable is nil. The zero value of any interface type in Go is nil, and error is no exception. When a function returns error, the convention is that nil means "no error" and any non-nil value means "something went wrong".

The errors package, on the other hand, is a regular package in the standard library. You import it with import "errors", and it provides helpers like errors.New, errors.Is, and errors.As. People sometimes confuse the builtin type error with the package errors. They're different things. The type is in the language; the package is one of several places in the standard library that produce and inspect values of that type.

Why Use an Interface at All?

Go could have used a fixed Error struct, or strings, or integer codes. Instead the language designers picked an interface, and that choice carries weight. An interface decouples the producer of an error from the consumer. The function returning an error doesn't have to use a single concrete type, and the caller doesn't have to care which concrete type came back, only that the value can describe itself with Error().

The diagram shows three different concrete types all flowing through the same error interface to the same caller. The caller doesn't know whether the value came from errors.New, fmt.Errorf, or a custom type defined in your own code. It just sees an error. That's the decoupling: the producing side picks the type that carries the right information, and the consuming side works against the interface.

This pays off in practice. A function in the os package returns a *os.PathError describing which file path failed. A function in strconv returns a *strconv.NumError describing which number couldn't be parsed. Both satisfy the error interface, so both can be returned through the same error slot and checked with the same if err != nil pattern. The error value can carry rich information for code that wants to look closer, while still being usable as a plain string for code that doesn't.

The if err != nil Check

The canonical way to handle an error in Go is to compare it against nil right after the call. If the comparison is false, an error happened and the caller decides what to do. If it's true, the call succeeded and the normal result is safe to use.

strconv.Atoi returns two values: an int and an error. The convention is to assign both, check the error first, and only use the result when the error is nil. If you flip the order and use qty before checking err, you might be working with the zero value of int while ignoring the fact that the conversion failed.

Here's the same code with an input that fails:

When strconv.Atoi fails, err holds a non-nil error value, and qty holds the zero value 0. The string "strconv.Atoi: parsing \"three\": invalid syntax" came from the Error() method of the concrete type the package returned, namely *strconv.NumError. The caller doesn't need to know that; printing the value with %v (or Println) automatically calls Error().

Why compare against nil and not something else? Because nil is the universal "no value" for an interface type. The standard library, the language itself, and every Go program in existence agree that error == nil means success. There's no other in-band signal. If a function wants to say "I failed", it returns a non-nil error. If it wants to say "I succeeded", it returns nil. The pattern is unambiguous, and the compiler doesn't have to add anything special to support it.

The flip side is that you have to actually write the check. Go doesn't force the caller to look at the error. You can write qty, _ := strconv.Atoi("three"), and the program will happily continue with qty == 0 and no warning. This is a deliberate trade-off: explicit error handling is verbose, but it's also visible. You can always see where errors are checked and where they aren't. Linters like errcheck flag the cases where a caller ignored an error, but the language itself doesn't.

The program runs, prints 0, and looks like it succeeded. It didn't. This is exactly why the if err != nil check is the first habit a Go developer builds. Don't ignore errors.

Error as the Last Return Value

When a Go function can fail, the error is always the last return value. This is convention, not a language rule, but it's followed so consistently across the standard library and the wider ecosystem that violating it makes code look wrong.

The function returns (float64, error), in that order. The caller destructures into two variables and checks the error first. If the function had additional successful results, those would all come before the error: (string, int, error), ([]Product, bool, error), and so on. The error is always the tail.

When there's no useful "successful" value, a function returns just an error. Save and delete operations look like this all the time:

The function returns just error. The caller doesn't get a "result" because there isn't one; the only question is whether the save worked. The if err := saveOrder(...); err != nil form combines the call and the check into one line, which keeps the success path uncluttered.

The reason for putting the error last is partly readability and partly composition. When you read qty, err := ..., your eye sees the result first, then the error. When you read a function signature like func Open(name string) (*File, error), the success type comes before the failure signal. It mirrors the way the call site reads. The convention has been stable for so long that breaking it (returning (error, float64), for example) makes the code feel off, and tools, IDEs, and reviewers all push back.

There's one edge case worth flagging. A function that returns just one error and no other value is common (saveOrder above, os.Remove, json.Unmarshal). A function that returns just one value, like (int, error), is also common. But a function that returns no error at all is making a promise: "I cannot fail". strings.ToUpper(s) doesn't return an error because it always succeeds for any input string. If you find yourself writing a function that always returns nil as the error, that's a hint the error return isn't needed.

How Standard Library Functions Return Errors

Most standard library packages follow the same shape: a function that can fail returns its useful result plus an error, and the caller checks the error before using the result. Recognizing this shape across the library makes the whole ecosystem feel coherent.

strconv.Atoi converts a string to an int. Its signature is func Atoi(s string) (int, error). Pass it "42" and you get back (42, nil). Pass it "forty-two" and you get back (0, *strconv.NumError).

Three different inputs go through the same loop. Some succeed ("3", "-5"), some fail ("ten", ""), and the loop handles each with the same if err != nil check. The error messages came from the concrete type the package returned; the calling code didn't need to know that type to print them. This is the interface decoupling at work.

os.Open is another classic. Its signature is func Open(name string) (*File, error). If the file exists and is readable, you get back the file handle and a nil error. If not, you get back a nil pointer and an error describing what went wrong:

The error message includes the operation (open), the path (does-not-exist.txt), and the underlying reason (no such file or directory). All of that detail came from *os.PathError, the concrete type os.Open returns. From the caller's point of view, it's still just an error value being checked and printed.

A subtle but important point: when a function returns (*File, error) and the call fails, the *File is nil. You're expected to check the error before using the result. If you skip the check and try to use the file anyway, you'll dereference a nil pointer and panic. Some Go APIs document this explicitly, others rely on the convention that a non-nil error usually means the other return values are not meaningful.

A table of common standard library functions and their error shape:

FunctionSignatureCommon failure
strconv.Atoi(s string) (int, error)Input isn't a valid integer
strconv.ParseFloat(s string, bitSize int) (float64, error)Input isn't a valid float
os.Open(name string) (*File, error)File doesn't exist, permission denied
os.ReadFile(name string) ([]byte, error)File doesn't exist, read error
json.Unmarshal(data []byte, v any) errorMalformed JSON, type mismatch
http.Get(url string) (*Response, error)Network error, invalid URL

Every one of these returns an error as the last return value (or as the only return value, when there's nothing else useful to hand back). Every one of these expects the caller to check the error before using the rest of the result. The shape is the contract.

Satisfying the Interface: A Quick Look

So far we've used errors that came from other packages. To finish the picture, here's what it takes to make your own type satisfy the error interface. We won't go deep here, custom error types get their own chapter, but seeing the mechanics once makes the rest of this section feel less abstract.

OutOfStockError is a regular struct. By defining a method Error() string on *OutOfStockError, the type now satisfies the error interface. The function reserve returns the pointer, and Go's type system accepts that as an error return value because *OutOfStockError has the required method.

The interface satisfaction is implicit. There's no implements error clause. The compiler checks at the assignment (return &OutOfStockError{...} in a function declared to return error) that the concrete type's method set covers the interface, and if it does, the assignment is valid. If you forgot to write the method, or wrote func (e *OutOfStockError) Message() string instead, the return statement would fail to compile with a clear message about the missing Error() string.

The diagram traces the path from a plain struct to a value usable as an error. The struct on its own is just data. Adding the Error() string method on *OutOfStockError is what makes the pointer type satisfy the interface. Once it satisfies, you can return it from any function declared to return error. No explicit "this type implements error" declaration is needed.

A practical note on receivers. The method Error() is defined on *OutOfStockError (pointer receiver), which means *OutOfStockError satisfies error, but OutOfStockError (the value type) does not. Most error types in Go use pointer receivers because errors are usually checked by identity (err == io.EOF) or compared with errors.Is/errors.As, and pointer receivers make those checks meaningful. For now it's enough to know that error is satisfied by whichever method set has Error() string.

The Typed Nil Interface Gotcha

There's one trap that bites people writing functions that return error. An interface value compared with nil can be false even when the concrete pointer inside it is nil. This shows up most often with error-returning functions that store their work in a typed local variable.

The mechanic: an interface value is internally two words, a type descriptor and a value. It compares equal to nil only when both words are nil. If you assign a typed nil pointer (like a *OutOfStockError that happens to be nil) to an error variable, the type word gets set even though the value word is nil. The interface as a whole is not nil.

The function meant to say "no error" by leaving e as a nil pointer. But because the return type is error, Go wraps the typed nil pointer in an interface value. The interface has its type word set to *main.OutOfStockError and its value word set to nil. That interface compares unequal to nil. The caller's if err != nil check fires when it shouldn't, and the program treats a successful call as a failure.

The fix is to return the untyped nil literal directly when there's no error, instead of a typed nil pointer:

Now the function returns the untyped nil literal when nothing went wrong, and only returns a typed pointer when there's a real error. The interface's type word stays empty on the success path, so err == nil is true and the caller sees the call as successful.

The rule of thumb: when a function returns error, don't return a typed nil variable. Either return the untyped nil literal directly, or guarantee that the typed variable was set to a real (non-nil) pointer before returning. The same gotcha exists for any interface return type, but it bites hardest with error because error returns are so common.

Recognizing this shape avoids a confusing class of nil-related bugs.

Putting It Together

The error interface is a single-method contract, and a small set of conventions builds the entire error-handling experience around it. Functions that can fail return (result, error), with error last. Callers check if err != nil immediately and either handle the error or pass it up. Any type that defines Error() string satisfies the interface, which is what lets the standard library, third-party packages, and your own code interoperate on the same error plumbing.

(Note: the formatted output for the third row reads input [ ] [1]: exactly, but Printf's %v on a [2]string shows both elements; the empty first element prints as nothing between the brackets.)

The function returns (CartLine, error). Each failure path returns a zero-value CartLine and a non-nil error. The success path returns a real CartLine and nil. The caller treats every iteration the same: check the error, branch on the result. Three different error sources (empty code, parse failure from strconv.Atoi, invalid value) all flow through the same error slot and the same if err != nil check. That's the point of the interface.