How to check whether a file or directory exists
Short answer: Path::new("notes.txt").exists() tells you whether anything is there — file, directory, or working symlink. is_file() and is_dir() tell you which. All three follow symlinks, and the answer can be stale by the time you act on it.
From a fresh project — cargo new exists-demo, then replace the contents of src/main.rs with each block below and run it with cargo run. Each block creates the files it works on, so it runs on its own and in any order. The fs::write calls at the top are fixtures; in real code the files would already exist.
exists, is_file, is_dir
Section titled “exists, is_file, is_dir”use std::fs;use std::path::Path;
fn main() -> std::io::Result<()> { fs::write("notes.txt", "meeting at ten\n")?; fs::create_dir_all("archive")?;
let notes = Path::new("notes.txt"); println!("notes.txt exists: {}", notes.exists()); println!("notes.txt is_file: {}", notes.is_file()); println!("notes.txt is_dir: {}", notes.is_dir());
let archive = Path::new("archive"); println!("archive exists: {}", archive.exists()); println!("archive is_file: {}", archive.is_file()); println!("archive is_dir: {}", archive.is_dir());
println!("missing.txt exists: {}", Path::new("missing.txt").exists()); Ok(())}notes.txt exists: truenotes.txt is_file: truenotes.txt is_dir: falsearchive exists: truearchive is_file: falsearchive is_dir: truemissing.txt exists: falseexists() answers “is something there at this instant”. is_file() and is_dir() are the same lookup plus a type check, for when “something” isn’t precise enough. All three are thin wrappers over fs::metadata, which follows symlinks — and that has a consequence the next section shows.
A broken symlink counts as “not there”
Section titled “A broken symlink counts as “not there””use std::fs;use std::os::unix::fs::symlink;use std::path::Path;
fn main() -> std::io::Result<()> { // Removed first so the block can run twice. let _ = fs::remove_file("link.txt"); let _ = fs::remove_file("broken.txt"); fs::write("target.txt", "real\n")?; symlink("target.txt", "link.txt")?; symlink("gone.txt", "broken.txt")?;
let link = Path::new("link.txt"); println!("link.txt is_file: {}", link.is_file());
let broken = Path::new("broken.txt"); println!("broken.txt exists: {}", broken.exists()); Ok(())}link.txt is_file: truebroken.txt exists: falselink.txt points at a file that exists, so is_file() follows it and says yes. broken.txt points at gone.txt, which was never created — so exists() reports false, even though the directory entry is sitting right there. If you want to ask about the link itself rather than its target, use path.symlink_metadata().is_ok(). The symlink helper here is the Unix one; on Windows the equivalent is std::os::windows::fs::symlink_file, and the follow-the-link behaviour is the same.
false can also mean “I couldn’t ask”
Section titled “false can also mean “I couldn’t ask””exists() is fs::metadata(path).is_ok(), so it collapses every outcome into one bit. A parent directory you can’t search reads the same as an absent path:
use std::fs;use std::os::unix::fs::PermissionsExt;use std::path::Path;
fn main() { // Removed first so the block can run twice. let _ = fs::remove_dir_all("locked-dir"); fs::create_dir_all("locked-dir/child").unwrap(); fs::set_permissions("locked-dir", fs::Permissions::from_mode(0o000)).unwrap();
let exists = Path::new("locked-dir/child").exists(); println!("exists: {exists}");
match Path::new("locked-dir/child").try_exists() { Ok(v) => println!("try_exists says: {v}"), Err(e) => println!("try_exists errored: {e}"), }
// Put it back so the block can run twice. fs::set_permissions("locked-dir", fs::Permissions::from_mode(0o755)).unwrap();}exists: falsetry_exists errored: Permission denied (os error 13)The first check says false — the same answer missing.txt got two sections ago. try_exists() on the very same path doesn’t guess: it hands back the reason, Permission denied (os error 13), as Err. Only its Ok(false) is a promise that the path isn’t there; Err means the question went unanswered. exists() is fine for deciding which of two config files to load; if your code acts on “doesn’t exist” by creating something, or by reporting a problem to a user, try_exists() is the honest one.
The check and the use are two moments
Section titled “The check and the use are two moments”Even Ok(true) doesn’t survive long. Between the check and whatever you do next, the file can vanish — another process, a user in a terminal, a temp cleaner. You can watch the gap open in a single process:
use std::fs;use std::io::ErrorKind;use std::path::Path;
fn main() { fs::write("notes.txt", "meeting at ten\n").unwrap(); let path = Path::new("notes.txt");
if path.exists() { // Stand-in for another process: the file is gone after the check. fs::remove_file(path).unwrap(); match fs::read_to_string(path) { Ok(text) => println!("read: {text:?}"), Err(e) if e.kind() == ErrorKind::NotFound => { println!("checked, then gone: {e}"); } Err(e) => println!("other error: {e}"), } }}checked, then gone: No such file or directory (os error 2)The check said yes and the read still failed. This is the delete page’s advice from the other side: when the next step is to open the path, skip the pre-check and open it — then match on ErrorKind::NotFound. The open and the read still aren’t atomic, but there’s no gap you built yourself. exists() earns its keep when there’s no follow-up operation to race with: choosing which config file to mention, deciding whether to draw a menu item, checking a directory before offering to scan it.
Related
Section titled “Related”- How to delete a file or a whole directory — handling
NotFoundinstead of checking first - Reading and writing files — the rest of the chapter