Skip to content
Select theme

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.

notes.txt exists: true
notes.txt is_file: true
notes.txt is_dir: false
archive exists: true
archive is_file: false
archive is_dir: true
missing.txt exists: false

exists() 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.

Section titled “A broken symlink counts as “not there””
link.txt is_file: true
broken.txt exists: false

link.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:

exists: false
try_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.

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:

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.