270. Path::try_exists — exists() Can't Tell "Missing" From "Couldn't Check"
path.exists() returns false for a file that’s missing — and for a file that’s right there behind a directory you can’t read. try_exists() finally separates the two.
Path::exists() is implemented as “did fs::metadata succeed?”. Any failure — permission denied on a parent directory, an I/O error, an interrupted syscall — collapses into false:
| |
If the app lacks permission to look inside /etc/app, this code concludes the config is absent and happily tries to overwrite it — or silently skips loading settings that exist. The error didn’t go away; it was converted into wrong control flow.
Path::try_exists() (stable since 1.63) returns io::Result<bool>, so “no” and “don’t know” are different answers:
| |
Ok(false) now means the path is definitely absent — the lookup succeeded and found nothing. Everything else is an Err you can log, retry, or propagate with ?.
Two details worth knowing:
- Broken symlinks report
Ok(false)—try_exists()follows symlinks, so a link pointing at nothing counts as “the target doesn’t exist”. Usesymlink_metadataif the link itself is what you care about. - TOCTOU still applies — any exists-then-act sequence can race with other processes. For “create only if absent”, skip the check entirely and use
OpenOptions::new().create_new(true)(bite 169), which makes the filesystem decide atomically.
Same theme as file_type() in bite 269: the std methods returning plain bool are quietly eating errors, and each has a Result sibling that doesn’t.