How to Delete a File or Directory in Java
Java 13 min read
Files.delete throws where File.delete returns false, a directory must be empty first and the JDK has no recursive delete, and deleteOnExit accumulates filenames for the life of the JVM.
Deleting a file has two APIs and one structural gap. The APIs differ in whether failure is an exception or a boolean; the gap is that there is no recursive delete in the JDK, which is a deliberate omission rather than an oversight.
Written against Java 17.
The two calls
Path path = Path.of("/var/data/report.csv");
Files.delete(path); // throws if it is not there
boolean removed = Files.deleteIfExists(path); // false instead
File file = new File("/var/data/report.csv");
boolean ok = file.delete(); // false for every kind of failure
Files.delete throws a typed exception naming the path and the reason:
try {
Files.delete(path);
} catch (NoSuchFileException e) {
// it was not there
} catch (DirectoryNotEmptyException e) {
// it is a directory with contents
} catch (AccessDeniedException e) {
// no permission, or the file is locked on Windows
}
File.delete() collapses all three into false. That is the reason to prefer the NIO version, when
a delete fails in production, “false” is not a diagnosis.
deleteIfExists returns false for a missing file and still throws for the other failures, which is
usually the right split for cleanup code: absence is a normal outcome, a permission problem is not.
Directories must be empty
Files.delete(directory); // DirectoryNotEmptyException if it has contents
Neither API deletes a non-empty directory, and there is no flag or option anywhere that changes it. The recursive version is a walk:
static void deleteRecursively(Path root) throws IOException {
if (Files.notExists(root)) return;
try (Stream<Path> walk = Files.walk(root)) {
walk.sorted(Comparator.reverseOrder()) // children before parents
.forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
Three things in there are load-bearing.
Comparator.reverseOrder() puts deeper paths first, because Files.walk yields parents before
children and a parent cannot be deleted while its children exist. Sorting the paths as strings
happens to give the right order, since a child path is always a prefix-extension of its parent.
The try-with-resources is mandatory. Files.walk holds open directory handles, and leaking them
exhausts the process’s file descriptors, a failure that surfaces much later somewhere unrelated.
UncheckedIOException exists because forEach cannot throw a checked exception. Wrapping and
unwrapping at the boundary is the standard workaround; an explicit loop avoids it entirely and is
arguably clearer.
An explicit loop is worth considering instead. It is a few lines longer, needs no
UncheckedIOException, and lets a failure on one entry be recorded rather than aborting the walk —
which is usually what a cleanup routine wants, since stopping halfway leaves the tree in a worse
state than either finishing or not starting.
The version that reports rather than aborting on the first failure:
static List<Path> deleteRecursivelyQuietly(Path root) throws IOException {
List<Path> failed = new ArrayList<>();
try (Stream<Path> walk = Files.walk(root)) {
walk.sorted(Comparator.reverseOrder()).forEach(p -> {
try { Files.delete(p); } catch (IOException e) { failed.add(p); }
});
}
return failed;
}
Why there is no Files.deleteRecursively
The omission is deliberate. A recursive delete is the most destructive operation a file API offers, and getting it wrong deletes things outside the intended tree, which is exactly what happens when it follows symbolic links.
try (Stream<Path> walk = Files.walk(root)) { } // does NOT follow links (default)
try (Stream<Path> walk = Files.walk(root, FOLLOW_LINKS)) { } // DOES — dangerous here
Files.walk does not follow links by default, which is the safe choice and worth not overriding. A
link inside the tree is then deleted as a link, leaving its target alone.
The other guard worth adding when the path came from outside the application:
Path base = Path.of("/var/app/work").toRealPath();
Path target = base.resolve(name).normalize();
if (!target.startsWith(base)) {
throw new IllegalArgumentException("Path escapes the working directory");
}
normalize() collapses .., so the check must come after it. Without both, a name containing
../../ deletes a tree somewhere else entirely.
deleteOnExit accumulates
file.deleteOnExit();
Two problems, both covered in more detail under temporary
files. The filename is added to a list held for the JVM’s entire
lifetime, so a server calling it per request leaks one string per request. And the hook runs only on
a clean shutdown: kill -9, a container stop that times out, or Runtime.halt all skip it.
Delete explicitly in a finally, or open the file with StandardOpenOption.DELETE_ON_CLOSE, which
also fires when an exception unwinds and is the most reliable of the three options.
Windows locks open files
try (InputStream in = Files.newInputStream(path)) {
// ...
}
Files.delete(path); // fine — the stream is closed
On Windows a file cannot be deleted while any handle to it is open, and the failure is
AccessDeniedException rather than anything mentioning a lock. On Linux the delete succeeds and the
data survives until the last handle closes, so the same code behaves differently.
A delete that fails only on Windows, or only under load, is almost always an unclosed stream, often
one leaked from a Files.lines without try-with-resources.
Memory-mapped files are worse: the mapping can outlive the channel and hold the file until garbage collection runs, which is why a mapped file sometimes cannot be deleted at all until the JVM exits.
Files.delete on Windows also fails on a file marked read-only, with the same
AccessDeniedException. Clearing the attribute first is the fix:
Files.setAttribute(path, "dos:readonly", false);
Files.delete(path);
That attribute view does not exist on POSIX systems, so guard it with a check of
supportedFileAttributeViews() in code that runs on both.
Deleting safely under concurrency
Every delete is a race. Between deciding to delete and the call landing, another process can remove the file, replace it, or create a directory at that path. The API handles this better than most code does:
if (Files.exists(path)) { // WRONG — the state can change in between
Files.delete(path);
}
Files.deleteIfExists(path); // one operation, decided by the filesystem
deleteIfExists is a single filesystem call, so there is no window. The exists check adds one and
buys nothing: the same time-of-check to time-of-use pattern covered under
checking whether a file exists.
The recursive walk has an unavoidable version of this: the tree can change while it is being walked.
Files.walk reads directory contents lazily, so an entry created after the walk passed its parent is
never visited, and the parent’s delete then fails with DirectoryNotEmptyException. For a directory
something else may write to, either take a lock or accept the retry:
for (int attempt = 0; attempt < 3; attempt++) {
try {
deleteRecursively(root);
return;
} catch (DirectoryNotEmptyException e) {
// something was written during the walk — go round again
}
}
Moving to a trash directory instead
For anything a person cares about, deleting is the wrong verb:
Path trash = base.resolve(".trash").resolve(Instant.now().toString() + "-" + path.getFileName());
Files.createDirectories(trash.getParent());
Files.move(path, trash, StandardCopyOption.ATOMIC_MOVE);
A move within the same filesystem is atomic and cheap, it rewrites a directory entry rather than touching the data, so this costs no more than the delete and is reversible. A scheduled sweep removes anything older than a retention window.
Related: copying, moving and checking existence. More in the Java guides.
Frequently asked questions
Files.delete or File.delete?
Files.delete. It throws a typed exception naming the path and the
reason, where the legacy method returns false for every kind of failure.
What is the difference between delete and deleteIfExists?
delete throws NoSuchFileException
for a missing file; deleteIfExists returns false. Both still throw for permission and
non-empty-directory failures.
How do I delete a non-empty directory?
Walk it in reverse order and delete each entry. There is no recursive delete in the JDK, deliberately.
Why reverse order?
Files.walk yields parents before children, and a directory must be empty
before it can be removed. Reversing puts the deepest paths first.
Do I need to close the Files.walk stream?
Yes. It holds open directory handles, and leaking them exhausts the file descriptors, which fails later in unrelated code.
Does the walk follow symbolic links?
Not by default, which is the safe behaviour for a delete —
a link is removed as a link and its target is untouched. Do not add FOLLOW_LINKS here.
Why does my delete fail on Windows but work on Linux?
Windows refuses to delete a file with an open handle. On Linux the delete succeeds and the data survives until the last handle closes. The cause is almost always an unclosed stream.
Should I use deleteOnExit?
Not in a long-running process. It retains every filename for the JVM’s lifetime and only runs on a clean shutdown.
How do I make sure a delete is reversible?
Move the file into a trash directory on the same filesystem instead. An atomic move costs the same and can be undone.
How do I safely delete a path supplied by a user?
Resolve it against a base directory, call
normalize(), then verify the result still starts with the base. normalize collapses .., so the
check must come after it.