#268 Jul 19, 2026

268. fs::read_dir — The Filesystem Doesn't Sort for You

fs::read_dir looks like ls, but ls sorts its output and read_dir doesn’t. You get entries in whatever order the filesystem feels like — and that order changes between machines.

The docs say it plainly: “The order in which this iterator returns entries is platform and filesystem dependent.” On your dev box the entries may happen to come back alphabetical; on ext4 with hashed directories, or in CI, they won’t. Code like this works until it doesn’t:

1
2
3
4
// Process migrations in order... on SOME machines
for entry in fs::read_dir("migrations")? {
    apply(&entry?.path()); // 002 may run before 001
}

The fix is to collect and sort — PathBuf is Ord, so it’s two lines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::fs;

let mut paths: Vec<_> = fs::read_dir("migrations")?
    .map(|res| res.map(|e| e.path()))
    .collect::<Result<Vec<_>, _>>()?;
paths.sort();

for p in &paths {
    apply(p); // 001, then 002, on every machine
}

Two details worth noticing along the way:

  • each item the iterator yields is a Result — reading an individual entry can fail even after opening the directory succeeded. Collecting into Result<Vec<_>, _> surfaces the first error instead of hiding it in the loop
  • DirEntry::path() hands back the entry joined to the base you passed in (migrations/001.sql, not 001.sql) — ready to open, no join needed

Sorting PathBufs compares component-wise, which is what you want for nested listings. Just remember it’s lexicographic: 10.sql sorts before 2.sql — zero-pad your file names or sort by a parsed key.

← Previous 267. Path::file_name — rsplit('/') Hands You Empty Strings and '..' Next → 269. DirEntry::file_type — Stop Paying a stat() Per Entry