skip to content

Standard Library Contracts

The standard library already fixed what io.EOF, fs.ErrNotExist and context.DeadlineExceeded mean, and your code has to honour those conventions instead of inventing its own.

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

explore

questions

13

What does ctx.Err() return after a context is cancelled versus after its deadline passes?

level: juniorimportance: must knowfreq 72%

answer

  1. three answers, never more
  2. one means time, one means intent
  3. the channel closes, the value latches
  4. wrapping breaks == but not errors.Is

basics

~10 s

ctx.Err() returns nil while the context is still live, context.Canceled once a cancel function has been called, and context.DeadlineExceeded once the context's deadline has passed. Match them with errors.Is, not with ==.

solid answer

~40 s

A `context.Context` has exactly three answers for `Err()`: `nil` while it is still active, `context.Canceled` if somebody called its cancel function, and `context.DeadlineExceeded` if its deadline expired first. Both are plain sentinel values, and `Done()` closing is the signal that one of them is now non-nil. The distinction is the whole point: `context.Canceled` means someone deliberately gave up (the caller disconnected, a sibling task failed, the user hit stop), while `context.DeadlineExceeded` means time ran out on its own. Always test with `errors.Is(err, context.Canceled)` rather than `err == context.Canceled`, because by the time the error reaches you it has usually been wrapped by a transport or by your own `fmt.Errorf("...: %w", err)`.

code

go · 10 lines
go
func fetch(ctx context.Context, in <-chan Result) (Result, error) {
	select {
	case r := <-in:
		return r, nil
	case <-ctx.Done():
		// context.Canceled if someone gave up,
		// context.DeadlineExceeded if time ran out.
		return Result{}, ctx.Err()
	}
}

go deeper

for a junior

Be ready to name all three return values of ctx.Err() — nil, context.Canceled, context.DeadlineExceeded — and to say plainly that one means someone gave up and the other means time ran out.

for a middle

Explain why errors.Is is required rather than ==, that Err() latches at the first cause and never changes, and that Done() closing is the same instant Err() becomes non-nil.

for a senior

Show the operational split: cancellations are usually normal traffic, deadline expiries are a health signal. Be ready to say what your service does differently with each and why returning ctx.Err() unaltered matters to callers.

for a principal

Own the convention across services: which sentinel is recorded on a span, what the error-rate SLI counts, and how you stop teams inventing bespoke timeout errors that break every caller's classification.

## The three states of ctx.Err() `context.Context` has four methods, and `Err() error` is the one that reports *why* the context finished. It answers one of three things: - **`nil`** — the context is still live. Nothing has cancelled it and no deadline has passed. - **`context.Canceled`** — someone called the cancel function associated with this context (or with one of its ancestors). - **`context.DeadlineExceeded`** — the context had a deadline and that moment arrived before anyone cancelled it. Those last two are package-level sentinel values: ```go var Canceled = errors.New("context canceled") var DeadlineExceeded error = deadlineExceededError{} ``` Once `Err()` is non-nil it never changes again — the first cause wins and is latched. `Done()` returns a channel that is closed at the same instant `Err()` becomes non-nil, which is why the standard shape of a cancellable operation is a `select` on `ctx.Done()` that then returns `ctx.Err()`. ## Why the two values are different, not interchangeable The two sentinels answer different operational questions, and this is why the standard library bothers to distinguish them. `context.Canceled` means *a decision was made*. Something upstream stopped wanting the result: the caller hung up, a parent task failed and tore down its children, a user navigated away. Nobody is waiting for your answer, so producing one is wasted work. It is almost never worth retrying, and it is usually not an alert-worthy error — logging every client disconnect as a failure is how a service ends up with a permanently red error rate. `context.DeadlineExceeded` means *time ran out*. The work was still wanted; it simply took longer than the budget allowed. That points at a slow dependency, a saturated pool, or a budget that is too tight — a genuine signal about the health of the system. ## Always use errors.Is Both values are sentinels, so a direct comparison works only when the error reaches you completely unwrapped. In practice it does not: transports and libraries wrap the cause, and your own code adds context with `%w`. The correct test is: ```go if errors.Is(err, context.DeadlineExceeded) { ... } if errors.Is(err, context.Canceled) { ... } ``` `errors.Is` walks the `Unwrap` chain, so it still matches when the sentinel is buried three layers down. `err == context.Canceled` silently returns false in exactly the situation you wrote the check for. This is the single most common bug on this topic. The symmetric mistake is *returning* something other than the sentinel. When your function gives up because the context finished, return `ctx.Err()` (optionally wrapped with `%w`) rather than inventing a `errors.New("timed out")`. Callers are matching on the sentinels; a hand-rolled string breaks every one of those checks. ## The dual identity of context.DeadlineExceeded `context.DeadlineExceeded` is not a bare `errors.New` value. Its concrete type implements `Timeout() bool` and `Temporary() bool`, both returning true, which means it satisfies the `net.Error` interface. Code that classifies network failures by asking `if ne, ok := err.(net.Error); ok && ne.Timeout()` will therefore also catch an expired context — deliberately, so that a context deadline looks like a timeout to generic network-error handling. `context.Canceled` has no such method: it is an ordinary `errors.New` value and `Timeout()` is not defined on it. So a cancelled context is *not* a timeout, by construction. ## For the engineer arriving from another language If your background is Java, Python or C#, a deadline probably arrives as a thrown exception — `TimeoutException`, `asyncio.TimeoutError`, `OperationCanceledException` — that unwinds the stack from wherever the blocking call was made. Go does none of that. Nothing is thrown, no goroutine is interrupted, and no stack is unwound. A context that finishes only closes a channel and sets `Err()`. Work stops only because your code is watching `ctx.Done()` and chooses to return, and the error only reaches the caller because you returned it. That is why a Go function that ignores its `context.Context` keeps running happily long after its caller has given up. The context is a broadcast that the answer is no longer wanted — not a mechanism that stops anything. ```go func fetch(ctx context.Context) (Result, error) { select { case r := <-resultCh: return r, nil case <-ctx.Done(): return Result{}, ctx.Err() // Canceled or DeadlineExceeded } } ``` That `return ctx.Err()` is the entire contract: the caller learns both that the work stopped and which of the two reasons stopped it.

  • Which of the two should page someone, and which should be logged quietly?
    `context.DeadlineExceeded` is the interesting one: the work was still wanted and the budget was blown, which points at a slow dependency or a too-tight deadline. `context.Canceled` usually means the caller walked away — a client disconnect, a cancelled request — and is normal traffic. Alerting on cancellations produces a permanently red error rate that nobody can act on.
  • If your function stops early because the context finished, what error should it return?
    Return `ctx.Err()`, or wrap it with `fmt.Errorf("...: %w", ctx.Err())`. Callers classify failures with `errors.Is(err, context.Canceled)` and `errors.Is(err, context.DeadlineExceeded)`; a hand-rolled `errors.New("timed out")` matches neither and quietly breaks every one of those checks.
  • Is context.DeadlineExceeded a net.Error?
    Yes. Its concrete type implements `Timeout()` and `Temporary()`, both returning true, so it satisfies `net.Error` and generic timeout classification catches it. `context.Canceled` is a plain `errors.New` value with no such methods, so a cancelled context is never reported as a timeout.

Canceled is the diner walking out; DeadlineExceeded is the kitchen missing the service window. Both stop the order, but only one is the kitchen's problem.

saying these in an interview costs you the question

  • Comparing with err == context.Canceled instead of errors.Is
  • Saying ctx.Err() returns the same value in both cases
  • Believing cancellation interrupts a running goroutine
  • Returning a custom errors.New instead of ctx.Err()
  • Treating client disconnects as service errors worth alerting on
open as a page

Why is io.EOF returned by a Read call treated as a normal end of input, not a failure?

level: juniorimportance: must knowfreq 82%

basics

~20 s

io.EOF is a sentinel error the io.Reader contract uses to say the stream is finished, so nothing went wrong. Callers detect it, stop reading and return success; they never log it or pass it on as a fault.

open as a page

Given an error from os.Open, how do you check that the file does not exist?

level: juniorimportance: must knowfreq 76%

basics

~10 s

Use errors.Is(err, fs.ErrNotExist). os.Open returns a *fs.PathError wrapping the operating system's error, so comparing the returned error directly against fs.ErrNotExist is always false; errors.Is unwraps the chain and finds the sentinel.

open as a page

Why can an io.Reader's Read return n > 0 bytes and io.EOF in the same call?

level: middleimportance: must knowfreq 58%

basics

~20 s

The io.Reader contract lets a reader that hits the end of its data while filling your buffer report both at once. Process the n bytes returned before you inspect the error, or the last chunk of the stream is silently dropped.

open as a page

Why doesn't errors.Is(err, context.DeadlineExceeded) match an expired socket read deadline?

level: middleimportance: should knowfreq 44%

basics

~10 s

They are two unrelated sentinel values. A read that fails because an I/O deadline expired wraps os.ErrDeadlineExceeded; only a finished context.Context yields context.DeadlineExceeded. Both report Timeout() true through the net.Error interface.

open as a page

What is the difference between io.EOF and io.ErrUnexpectedEOF, and when does io.ReadFull return each?

level: middleimportance: should knowfreq 44%

basics

~20 s

io.EOF means the stream ended where an end was allowed; io.ErrUnexpectedEOF means it ended mid-item with bytes still owed. io.ReadFull returns io.EOF if it read nothing at all, and io.ErrUnexpectedEOF if it read some but not all.

open as a page

Why can os.IsNotExist return false for an error that errors.Is(err, fs.ErrNotExist) matches?

level: middleimportance: should knowfreq 54%

basics

~20 s

os.IsNotExist predates error wrapping. It peels exactly one layer, and only for the os package's own wrapper types. It never follows an Unwrap chain, so a fmt.Errorf %w wrapper defeats it while errors.Is still matches.

open as a page

Your RPC spans show context.DeadlineExceeded at one hop and context.Canceled at the next — what does each value tell you about where the deadline was enforced?

level: seniorimportance: should knowfreq 50%

basics

~20 s

context.DeadlineExceeded marks the hop whose own context chain ran out of time; context.Canceled marks a hop torn down because someone upstream had already given up. The deadline was enforced at the first hop, and the second was collateral.

open as a page

A Go ingest job read a truncated line-delimited feed as a complete batch with no error. How do you locate the faulty end-of-stream check?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Reproduce it with a deliberately truncated fixture first, then audit where the loop decided it was finished: an unchecked bufio.Scanner.Err, or a break on any error, both turn a failure into a clean end. Then compare bytes actually read against the length the source promised.

open as a page

A restore tool recreates any file whose os.Stat call returns an error, and a customer reports overwritten data. How do you diagnose and fix that check?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The check conflates absence with every other failure. A permission denial on an unreadable directory looks identical to a missing file, so it recreates data that was there. Match only fs.ErrNotExist for absence and fail on anything else.

open as a page

What does context.Cause(ctx) tell you that ctx.Err() cannot?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

ctx.Err() only ever reports context.Canceled or context.DeadlineExceeded. context.Cause returns the specific error a WithCancelCause cancel func was given, so you learn why the context was cancelled, not merely that it was.

open as a page

What do the Op and Path fields of *fs.PathError give you, and how do you reach them?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

Op names the operation that failed ("open", "stat", "mkdir") and Path the name it was given, so a log can carry them as separate structured fields instead of parsing a message. Reach them with errors.As into a *fs.PathError variable.

open as a page

Why must a custom io.Reader return io.EOF itself rather than a wrapped end-of-stream error?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Standard library stream code detects the end by comparing the error to io.EOF by identity, not by unwrapping it. A wrapped or custom ending therefore surfaces as a real failure from io.Copy and from a scanner, so return io.EOF unchanged.

open as a page