sxyazi/yazi · error · io::Error

trash directory is not empty

Error message

trash directory is not empty

What it means

Thrown by `Trash::remove_dir` on Windows when deleting a nested trashed directory (an entry with `has_rel()`) that still contains children. The implementation deletes top-level Recycle Bin items via the shell API directly, but nested directories must be empty before `item.delete()` is attempted — yazi does not recursively purge nested trash content on your behalf here.

Source

Thrown at yazi-fs/src/trash/windows/trash.rs:91

		let cha = if let Some(entry) = entry {
			TrashSig::item(&self.resolve(entry)?)?
		} else {
			TrashSig::root()?
		};

		Ok(if cha.hits(current.cha) { None } else { Some(File { cha, ..current.clone() }) })
	}

	pub(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> {
		self.resolve(entry)?.delete()
	}

	pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> {
		let item = self.resolve(entry)?;
		if !entry.has_rel() {
			item.delete()
		} else if !item.is_empty()? {
			Err(io::Error::new(io::ErrorKind::DirectoryNotEmpty, "trash directory is not empty"))
		} else {
			item.delete()
		}
	}

	pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> {
		for entry in entries {
			let to = entry.original.as_deref().ok_or_else(|| {
				io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
			})?;

			self.restore_do(&self.resolve(&entry)?, to)?;
		}
		Ok(())
	}

	pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> {
		fs::rename(&entry.backing, path)

View on GitHub (pinned to 94abcfa92f)

Solutions

  1. Remove the children first: `trash.list(Some(&entry))` and recursively `remove_file`/`remove_dir` each child, then remove the parent
  2. To purge everything at once, use `trash.empty()` (SHEmptyRecycleBinW) instead of per-entry removal
  3. Catch `DirectoryNotEmpty` and re-list/retry once to handle races with concurrent bin changes

Example fix

// before
trash.remove_dir(&entry).await?;

// after
for child in trash.list(Some(&entry))? {
    if child.lcha.is_dir() { trash.remove_dir(&child).await?; }
    else { trash.remove_file(&child).await?; }
}
trash.remove_dir(&entry).await?;
Defensive patterns

Strategy: fallback

Validate before calling

// Rust: pre-check emptiness
let is_empty = trash.list(Some(&entry)).map(|c| c.is_empty()).unwrap_or(false);
if is_empty { trash.remove_dir(&entry)?; } else { /* recurse or empty() */ }

Try / catch

match trash.remove_dir(&entry) {
    Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => {
        for child in trash.list(Some(&entry))? { /* remove child first */ }
        trash.remove_dir(&entry)?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `trash.remove_dir(&entry)` where `entry.has_rel()` is true and `item.is_empty()?` returns false — i.e., deleting a subdirectory inside the trash that still has files or folders.

Common situations: Deleting a folder from within the trash panel while its children were never removed; bulk operations that remove parents before children; a race where new children appear between listing and deletion.

Related errors


AI-assisted analysis of sxyazi/yazi@94abcfa92f (2026-08-16). Data as JSON: /api/errors/72fb88aa277f074e. Report an issue: GitHub.