ErrLookupBackground articles › Rust io::Error: what ErrorKind::NotFound, PermissionDenied, and InvalidData actually mean — from real OS failures to library fail-closed refusals

Rust io::Error: what ErrorKind::NotFound, PermissionDenied, and InvalidData actually mean — from real OS failures to library fail-closed refusals

io::Error is the error type Rust programs surface when input or output fails — a missing TLS certificate file, an unwritable temp directory, a truncated build archive, a rename blocked by a cloud-sync placeholder. This family covers both faces of the type across crates such as rustfs, rustc, cargo, Rocket, yazi, exa, and rustdesk: genuine OS errors (errno and Windows error codes mapped to ErrorKind) that libraries re-wrap with context, and library-minted errors built with io::Error::new to express fail-closed invariants such as reparse-point refusals, identity-change races, erasure-shard inconsistency, and framing violations. Developers meet it during file reads and writes, renames, canonicalization, TLS setup, archive building, and DRM capture; the same ErrorKind can be a raw OS verdict in one library and a deliberate refusal in another, so the message and library docs decide which.

Distilled from 252 documented records across 12 repositories.

Background

std::io::Error is Rust's built-in type for failed input and output, and it is produced at two very different layers. The classic form wraps an OS error code — errno on Unix, GetLastError on Windows — and kind() maps that code onto a coarse ErrorKind such as NotFound, PermissionDenied, InvalidInput, InvalidData, Unsupported, or Other; callers branch on the kind for control flow while Display prints the system message. The second form is arbitrary: any library can mint an io::Error with io::Error::new(kind, message), which has made the type the de facto error currency for anything that touches files, streams, or devices.

One half of the documented family is ordinary OS failure, relabeled for diagnosis. Rocket re-wraps failures opening TLS certificate files and keeps the underlying kind; yazi re-wraps cache-stamp write errors with io::Error::new(e.kind(), ...) so PermissionDenied or StorageFull survives the wrap; rustc's archive builder prefixes context strings like 'failed to open object file' or 'failed to map object file' onto the raw error; and cargo's Windows-only try_canonicalize synthesizes an explicit NotFound when canonicalization fails and the path genuinely does not exist. From the caller's side these behave like OS errors with better labeling: the real fix is the path, the permission, the disk, or the share.

The other half never came from the OS at all. Libraries deliberately pick a kind and message to express invariants: rustfs rejects reparse points (junctions, symlinks, mount points, cloud placeholders) as PermissionDenied, aborts file-identity-change races mid-open as InvalidData, enforces a rename jail against '..' components as InvalidInput, and fails closed on volumes without stable 64-bit file IDs as Unsupported. exa returns ErrorKind::Other from a CString path conversion whose message names a NUL byte that cannot occur in real Unix filenames — the actual trigger is almost always non-UTF-8 bytes in the name. cargo's LimitErrorReader converts silent truncation into an explicit error, and rustdesk bounds DRM scanout stride before row-copy arithmetic can overflow. Because the library chooses the kind, ErrorKind alone cannot tell an OS verdict from a library refusal; the message and the library's documentation have to.

The family also varies in shape. Some records chain two failures into one message, such as rustfs's '{write_err}; failed to schedule staged file cleanup: {cleanup_err}', which means the original write error plus a leaked staging file. Many checks are platform-gated — a large share of rustfs's guards exist only on Windows, and rustc's file locking simply reports 'file locks not supported on this platform' on targets without a backend. Retryability is library-specific: rustfs treats identity and framing errors as fail-closed signals that must not be patched around, while its final-path sizing race and staged-file interference are explicitly safe to retry with a fresh handle or a new staging name.

Common causes

What usually fixes it

Go deeper

Documented occurrences

…and 232 more across the corpus — use search.

Honest provenance: generated on 2026-08-16 from AI-assisted analysis of the linked records. See how records are made.