RFA-180 · Case file with fixtures · Case 152 of 694 · Runtime evidence
Why Path::exists Can Turn a Filesystem Error Into false
Path::exists is a convenience boolean that coerces metadata errors to false. Use try_exists when unknown must remain different from absent, and operate directly when a separate existence check would create a race.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- Unix evidence fixture; error-coercion contract is cross-platform
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- exists is a convenience query that converts filesystem metadata errors to false, merging confirmed absence with indeterminate access.
- First discriminating check
- Compare exists with try_exists on the same failing metadata lookup and retain the Result instead of treating every false as NotFound.
A boolean can be convenient because it removes decisions. At a filesystem boundary, the removed decision may be the most important information.
The failing program creates a Unix symbolic link pointing to itself. Resolving it fails with FilesystemLoop. Path::exists() returns false, the same value it would return for a confirmed missing path.
exists deliberately coerces errors
Path::exists follows symbolic links and returns false when metadata cannot be accessed. Its documentation warns that the method may be error-prone and describes it as a convenience operation that coerces errors to false.
This gives a two-state interface over a three-state reality:
confirmed present true
confirmed absent false
could not determine false
For optional decoration in a user interface, this may be acceptable. For installation, authorization, migration, or recovery logic, merging absent with unknown can cause the wrong action.
try_exists keeps the third state
The repaired program calls Path::try_exists. The result distinguishes Ok(true), Ok(false), and Err(error).
In the fixture, the error is the evidence. The code does not claim that the path is missing.
Other causes can include permission failures, an inaccessible parent, malformed target-specific paths, transient I/O errors, or a symlink-resolution loop. A useful diagnostic retains both the path operation and the original error.
Broken links and unreadable destinations differ
exists follows a symbolic link. A broken link therefore returns false even though the directory contains a link entry. If I need to ask whether the link itself exists, symlink_metadata or Path::is_symlink represents a different query.
This distinction matters during cleanup and deployment. “Destination does not resolve” does not mean “there is no directory entry to replace.”
I name the property I am testing: destination existence, entry existence, regular-file type, or openability. The word exists alone is often too broad.
A better check may be no check
Even try_exists cannot prevent a time-of-check/time-of-use race. The filesystem can change after the result and before the next operation.
If I want to read a file, I call File::open and handle its result. If I want to create only when absent, I use an atomic create-new operation. If I want to replace, I use the platform's replacement primitive and handle its errors.
The direct operation answers the question that matters at the moment it matters. A preliminary boolean often adds a race without making the later call optional.
Error policy belongs at the boundary
Sometimes an application intentionally treats permission denial as “not visible to this user.” That can be a valid product policy, but I express it by matching specific error kinds near the boundary.
Silently converting every future error to absence makes operational problems look like normal data state. A damaged mount or exhausted resource can then trigger creation, deletion, or misleading “not found” responses.
Logs should not reveal sensitive filesystem paths to untrusted clients, but internal telemetry can preserve a classified cause and operation.
Reproduction needs a reliable error
Permission-based tests are fragile because they behave differently under root, containers, ACLs, and operating systems. This case uses a self-referential Unix symlink because it reliably makes destination resolution indeterminate on the tested target.
The evidence is target-labelled. The general coercion rule comes from the cross-platform API contract, while the exact OS error code is not treated as universal.
This distinction—portable mechanism, target-specific reproduction—is important across the Atlas.
My filesystem-query checklist
When I see an existence test, I ask:
- Does unknown need to remain different from absent?
- Am I asking about the directory entry or the followed destination?
- Will I perform another filesystem operation immediately afterwards?
- Can that operation itself make the decision atomically?
- Which errors may be intentionally mapped to product-level absence?
- Is the test reliable under the privileges and target where it runs?
The core principle is information preservation. A boolean is only correct when the caller truly needs two states. Path::exists throws error detail away by design; try_exists preserves uncertainty, and a direct filesystem operation often avoids the separate check entirely.