skip to content

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

level: middleimportance: should knowfreq 44%

answer

  1. two clocks, two sentinels
  2. identity, not resemblance
  3. one interface spans both
  4. Timeout() true on either side
  5. errors.As unwraps where an assertion cannot

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.

solid answer

~40 s

Go has two separate timeout sentinels and they never match each other. When an I/O deadline set on a connection or a file expires, the failing read or write returns an error wrapping `os.ErrDeadlineExceeded`, testable with `errors.Is(err, os.ErrDeadlineExceeded)`. `context.DeadlineExceeded` comes only from a `context.Context` whose deadline passed. `errors.Is` compares sentinel identity, so a cross-check between the two is always false, no matter how similar the failures look. What unifies them is the `net.Error` interface: both satisfy it and both return true from `Timeout()`, so `var ne net.Error; errors.As(err, &ne) && ne.Timeout()` catches either. Pick the specific sentinel when you need to know which clock ran out, and the interface when you only need to know it was a timeout.

code

go · 11 lines
go
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
_, err := conn.Read(buf)

if errors.Is(err, os.ErrDeadlineExceeded) {
	// the I/O deadline fired — my own socket budget
}

var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
	// coarse: some deadline expired, unspecified which
}

go deeper

for a junior

Know that Go has more than one timeout sentinel and that you test any of them with errors.Is rather than ==, because the real error arrives wrapped.

for a middle

Explain that errors.Is matches sentinel identity, so os.ErrDeadlineExceeded and context.DeadlineExceeded never match each other, and that net.Error's Timeout() is the shared coarse contract.

for a senior

Demonstrate using the specific sentinel as evidence: which layer's deadline actually bound, and therefore which budget to change, rather than collapsing every expiry into one 'timeout' bucket.

for a principal

Set the house rule on failure vocabulary — that libraries return standard sentinels rather than bespoke timeout errors — so classification code written by one team keeps working against another team's package.

## Two timeout sentinels, two different clocks Go expresses "time ran out" in more than one place, and the values are deliberately distinct. **`context.DeadlineExceeded`** is produced by the `context` package. It appears as `ctx.Err()` when a context with a deadline reaches it, and it propagates to every context derived from that one. It says: *a deadline in this call chain's context expired.* **`os.ErrDeadlineExceeded`** is produced by the I/O layer. When a deadline has been set on a connection or a pollable file and a read or write is still pending when that moment arrives, the operation fails immediately with an error that wraps this sentinel. It says: *this particular I/O operation ran past its own deadline.* The documented test is `errors.Is(err, os.ErrDeadlineExceeded)`. They are not aliases and neither wraps the other. `errors.Is` matches on the identity of the target sentinel while walking the `Unwrap` chain, so `errors.Is(err, context.DeadlineExceeded)` on a socket deadline error is false, and `errors.Is(err, os.ErrDeadlineExceeded)` on a finished context's error is false too. A candidate who assumes one check covers both writes a classifier that silently misses half its cases. ## What you actually get back from the I/O layer The error is rarely the bare sentinel. A network read failure arrives as an `*net.OpError` — a struct carrying `Op` ("read"), `Net`, `Source`, `Addr` and the underlying `Err` — and the deadline sentinel sits inside it. That is exactly why you use `errors.Is` rather than `==`: the wrapper is informative and you want to keep it in the message while still matching the cause. ```go _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) _, err := conn.Read(buf) if errors.Is(err, os.ErrDeadlineExceeded) { // the socket's own deadline fired } ``` ## The net.Error interface is the common contract `net.Error` is the interface that unifies them: ```go type Error interface { error Timeout() bool Temporary() bool // deprecated } ``` An `*net.OpError` wrapping an expired deadline reports `Timeout() == true`. So does `context.DeadlineExceeded`, whose concrete type implements both methods and returns true from each — the `context` package went out of its way to make an expired context look like a timeout to generic network-error handling. `context.Canceled`, by contrast, is a plain `errors.New` value with no methods, so it is *not* a `net.Error` and never reports a timeout. That asymmetry is the point: a cancellation is a decision, not a timeout. The idiomatic interface check uses `errors.As`, which unwraps: ```go var ne net.Error if errors.As(err, &ne) && ne.Timeout() { // some deadline expired; this does not say which one } ``` A plain type assertion `err.(net.Error)` only works on the outermost error and misses a wrapped one — a common bug in older code. Ignore `Temporary()`. It is deprecated, was never consistently implemented, and "temporary" was never given a meaning callers could rely on. ## When the distinction actually matters Suppose an RPC transport sets an I/O deadline on its connection *and* the incoming request carries a context deadline. Both can expire, and which sentinel you get tells you which mechanism enforced the stop: - `os.ErrDeadlineExceeded` — your own socket-level deadline fired. The budget you configured on the connection was the binding constraint. - `context.DeadlineExceeded` — the deadline that travelled with the request expired. The caller's budget was binding, and often the socket deadline was set too generously to ever matter. - `context.Canceled` — nobody's clock ran out; something upstream gave up first. Collapsing all three into "timeout" is how a team spends a week tuning the wrong knob. The specific sentinel is the evidence for which layer to change. ## Choosing which check to write Use `errors.Is` with a specific sentinel when the answer changes what you do: which timeout to raise, which layer to blame, whether to attribute the failure to the caller or to yourself. Use the `net.Error` interface via `errors.As` when you genuinely only need the coarse question — is this a timeout at all — for example when deciding whether a failure is even a candidate for another attempt, or when tagging a span with a broad failure class. And if you are writing a library, return the standard sentinels rather than inventing your own. Every Go caller already knows `os.ErrDeadlineExceeded` and `context.DeadlineExceeded`; a bespoke `ErrTimeout` in your package is one more thing every consumer must learn and match separately.

  • Which errors satisfy net.Error and report Timeout() as true?
    Network operation errors wrapping an expired I/O deadline do, and so does `context.DeadlineExceeded` — its concrete type implements `Timeout()` and `Temporary()`, both returning true. `context.Canceled` does not: it is a plain `errors.New` value with no methods, so a cancellation is never classified as a timeout.
  • Why use errors.As for the net.Error check rather than a type assertion?
    A type assertion `err.(net.Error)` inspects only the outermost error. Real failures arrive wrapped — by an `*net.OpError`, by a transport, by your own `%w` — so the assertion fails on errors that do implement the interface underneath. `errors.As` walks the chain and assigns the first match.
  • Should you rely on net.Error's Temporary() method?
    No. `Temporary()` is deprecated and was never implemented consistently across the standard library or third-party code, and "temporary" was never given a definition a caller could act on. Classify with `Timeout()` plus the specific sentinels, and carry any retryability decision in your own error values.

A parking meter expiring and a court summons deadline are both 'time ran out', but you do not pay them at the same desk. Matching one against the other never succeeds.

saying these in an interview costs you the question

  • Assuming one errors.Is check catches both timeout sentinels
  • Using err.(net.Error) instead of errors.As on wrapped errors
  • Branching on the deprecated Temporary() method
  • Comparing with == against os.ErrDeadlineExceeded
  • Inventing a package-local ErrTimeout instead of the stdlib sentinels