skip to content

Since Go 1.24 crypto/rand.Read never returns an error. What changed, and how should code handle it?

level: middleimportance: nice to knowfreq 31%

answer

  1. the signature could not change
  2. the documentation changed instead
  3. an untested branch is a dangerous branch
  4. failure now stops the process
  5. one package variable can bring the error back

basics

~20 s

Go 1.24 made crypto/rand.Read always fill the whole slice and never report an error: a failure of the system's random source now stops the program. No error branch is left to write, and no fallback is justified.

solid answer

~50 s

The signature is still `func Read(b []byte) (n int, err error)` for compatibility, but the documented contract changed in Go 1.24: `Read` always fills `b` entirely and never returns an error. Underneath, a failure of the operating system's random source is now treated as fatal — the process dies rather than handing back a short or empty buffer. The motivation is that the old error path was untestable and actively harmful: it invited an `if err != nil` branch containing a fallback to `math/rand`, which turns an impossible failure into a guaranteed weakness. So `rand.Read(b)` on its own line is now correct; write `_, _ = rand.Read(b)` if a linter insists on the returns being consumed. One caveat: the guarantee is about the package's default source. If you assign your own `io.Reader` to `crypto/rand.Reader`, `Read` calls `io.ReadFull` on it and can fail again.

code

go · 18 lines
go
import (
	"crypto/rand"
	mrand "math/rand/v2"
)

// Wrong: an unreachable branch that silently downgrades the source.
func badFill(b []byte) {
	if _, err := rand.Read(b); err != nil {
		for i := range b {
			b[i] = byte(mrand.UintN(256))
		}
	}
}

// Right on Go 1.24 and later: the call fills b or the program stops.
func fill(b []byte) {
	rand.Read(b)
}

go deeper

for a junior

Know that filling a byte slice from crypto/rand is a single call and that on current Go there is no error worth handling. Do not invent a backup plan for when it fails.

for a middle

Explain why the error was retired rather than kept: the condition cannot occur on a running system, the branch was untestable, and in practice it filled up with fallbacks to a statistical generator. Note that the signature stayed for compatibility.

for a senior

Bring the operational angle: a failure here should take the process down, monitoring should show it, and no degraded mode should exist. Be able to point at a replaced crypto/rand.Reader as the realistic way this call starts returning attacker-friendly values.

for a principal

Argue the general lesson about API design under your own control: an error nobody can trigger is an invitation to write unsafe recovery code. Decide as a matter of policy that secret generation has no fallback path and that the service fails closed.

## What the contract says now ```go func Read(b []byte) (n int, err error) ``` The signature has not changed and cannot change — it is part of the standard library's compatibility promise. What changed in Go 1.24 is the documented behaviour: `crypto/rand.Read` always fills `b` completely and never returns an error. The two return values are still there; they are simply no longer interesting. ## Why the error was removed rather than kept On every operating system Go supports, the randomness source is a kernel generator that cannot fail on a running system. It is seeded once at boot and then produces bytes indefinitely; there is no pool to exhaust and no per-call failure mode to report. The error return was therefore describing a condition that essentially never occurred. An error that never occurs is worse than no error at all, for two reasons. First, it is untestable. Nobody exercises the branch, so whatever is written in it is unreviewed code that will run for the first time in the worst possible circumstances. Second — and this is the reason the change was made — engineers fill that branch with a fallback. The shape is depressingly consistent: `if _, err := rand.Read(b); err != nil { /* use a seeded generator instead */ }`. The author's instinct is availability: the service must keep issuing tokens. The effect is that a condition which would have been loud and obvious becomes silent and catastrophic — the service keeps running and every token it mints from then on is predictable. Removing the error removes the invitation. So the failure is now handled the only way that is actually safe: the program stops. It is not a recoverable panic you can wrap in a deferred `recover` and paper over. If the machine cannot produce random bytes, a service that mints reset tokens has nothing useful to do. ## What to write ```go key := make([]byte, 32) rand.Read(key) ``` That is complete and correct. If a linter in your pipeline flags unused return values, `_, _ = rand.Read(key)` satisfies it without implying there is something to check. Keeping an `if err != nil` around the call is not a bug — it is dead code — but what goes *inside* it matters enormously, and the only defensible content is a hard stop, which the runtime already does for you. On Go versions before 1.24, the check earned its keep: a short read was at least theoretically possible, and code that ignored `n` could proceed with a partly zero-filled buffer. If you maintain a library that still supports older toolchains, keep the check; just make sure the branch aborts rather than substituting a weaker source. ## The one remaining way it can fail `crypto/rand.Reader` is a package-level variable of type `io.Reader`. If it still holds the default value, `Read` goes straight to the operating system and cannot fail. If something has assigned a different reader to it, `Read` calls `io.ReadFull` on that reader instead, and whatever error that reader returns comes back to you. This matters in practice because replacing `Reader` is a known testing trick: swap in a deterministic reader so a test can assert on a fixed token. It is a sharp tool. A test that mutates a package-level variable affects every other test in the process, and a build tag or an initialisation path that leaves the replacement wired up in a production binary is a genuine incident — every value the service generates becomes the deterministic sequence the test wanted. Prefer designing the code so the source is injected as a parameter, and if you do replace `crypto/rand.Reader`, restore it with a deferred call in the same test. ## The reviewer's checklist - Is there an error branch around `crypto/rand.Read`? If so, does it contain a fallback generator? That is the bug. - Does any code use `n` from the return to decide how much of the buffer is valid? Since 1.24 that is always `len(b)`; before it, ignoring a short read was a real defect. - Is `crypto/rand.Reader` assigned anywhere outside a test? That is where a "never fails" call starts failing, or worse, silently succeeding with values somebody chose.

  • What actually happens at run time if the operating system's random source really does fail?
    The program is brought down rather than continuing with partial or weak data. It is not an error you can inspect and not something a deferred `recover` in your handler can absorb, which is deliberate: a service whose job is minting secrets has no safe degraded mode. Treat it as an infrastructure failure to investigate, not an application error to handle.
  • Does this make existing code that checks the error wrong?
    The check itself is harmless dead code and still compiles. What may be wrong is the contents of the branch: a fallback to a statistical generator was always a defect and is now unambiguously one. If the library must build on toolchains older than 1.24, keep the check and make the branch abort.
  • Does crypto/rand.Read block or slow down under heavy load?
    No. It reads from the kernel's generator, which produces bytes on demand; there is no pool that drains and no waiting once the system is up. The old advice about entropy starvation describes a different mechanism from a different era. Benchmark before optimising, and never optimise by seeding a faster generator from it.

saying these in an interview costs you the question

  • Puts a math/rand fallback in the error branch
  • Believes the function signature changed in Go 1.24
  • Uses the returned count to decide the buffer is short
  • Thinks crypto/rand.Read blocks waiting for entropy
  • Recovers from the failure and continues with a zeroed buffer
  • Leaves a replaced crypto/rand.Reader wired into a production build