Deleting Files in Java
Delete files and directories in Java with File.delete, Files.delete, and Files.deleteIfExists.
Deleting Files in Java
Three small calls, one big difference. File.delete() returns a boolean for everything from success to permission denied; Files.delete() throws specific exceptions for each failure; Files.deleteIfExists() is the middle option — boolean for the "did I delete anything?" question only, exceptions for the real failures. Picking the right one is mostly about how much you care about why the delete failed.
Also covered: removing a non-empty directory tree (none of the three calls does this on its own), the DELETE_ON_CLOSE open option for scratch files, and the safe "move then delete" pattern for replacing a file atomically.
File.delete() — legacy, returns boolean
File f = new File("notes.txt");
boolean ok = f.delete(); // true on success
// false on every failure: missing, locked, perms, non-empty dirThe legacy gap, same shape as mkdir and renameTo: a single boolean for every outcome. The call returns false if the file didn't exist, if you lacked permission, if the path was a non-empty directory, or — on Windows — if another process held the file open. You can't tell which from the return value.
File.delete() removes:
- A regular file.
- An empty directory. Non-empty directories return
false. - A symbolic link itself (not the target).
If you only need "file is gone by the end of the call," check f.exists() afterwards instead of relying on the return:
f.delete();
if (f.exists()) throw new IOException("could not delete " + f);That pattern is what you'll find replacing it across most older codebases.
Files.delete(path) — modern, throws
The java.nio.file peer trades the boolean for specific exceptions:
Path p = Path.of("notes.txt");
Files.delete(p);
// throws NoSuchFileException — the file didn't exist
// throws DirectoryNotEmptyException — path was a non-empty directory
// throws AccessDeniedException — permission denied
// throws IOException — anything else (locked, OS error, network FS hiccup)The exception types are subclasses of IOException, so a broad catch (IOException e) still works. The difference is that real error handling can be specific:
try {
Files.delete(path);
} catch (NoSuchFileException e) {
// already gone — not an error in the "delete if there" pattern
} catch (DirectoryNotEmptyException e) {
// need a recursive delete; handled below
}If "already gone" is fine, use Files.deleteIfExists instead of catching NoSuchFileException.
Files.deleteIfExists(path) — the middle option
boolean deleted = Files.deleteIfExists(path);
// returns true — the file existed and was deleted
// returns false — the file did not exist
// throws DirectoryNotEmptyException, AccessDeniedException, etc. for real failuresThe boolean here only distinguishes "I removed something" from "nothing to remove." Real errors still throw. That's the call you want for setUp / tearDown code, idempotent cleanup, and "delete this old marker if it's there" patterns:
Files.deleteIfExists(Path.of("lock")); // safe whether the lock was there or notUse the chart:
| You want… | Use |
|---|---|
| Legacy boolean, no error info | File.delete() |
| "Delete or tell me why it failed" | Files.delete(path) |
| "Delete if there; quiet if not" | Files.deleteIfExists(path) |
Deleting a non-empty directory tree
None of the single-call deleters removes a non-empty directory. There are two standard patterns:
Pattern 1: walk and delete in reverse. Files.walk yields a Stream<Path> in arbitrary order; sorting in reverse pushes leaves to the front so each parent is empty by the time you reach it.
try (Stream<Path> walk = Files.walk(root)) {
walk.sorted(Comparator.reverseOrder())
.forEach(p -> {
try { Files.delete(p); } catch (IOException e) { throw new UncheckedIOException(e); }
});
}Concise and the version most code uses today. The tradeoff: it loads every path into the sort. For directories with millions of entries, prefer pattern 2.
Pattern 2: Files.walkFileTree with a SimpleFileVisitor. The visitor pattern lets you delete leaves on visit and parents on postVisitDirectory, no sorting required:
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes a) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});Same outcome, no in-memory sort, more lines. The visitor API is covered in the Walk file tree chapter.
There is no built-in rmrf in the JDK. Both patterns above are the standard substitutes; many codebases ship a small Files.deleteRecursively(root) helper on top of one of them.
DELETE_ON_CLOSE — scratch files that clean themselves up
For "I need a file just while this stream is open," the StandardOpenOption.DELETE_ON_CLOSE option deletes the file when the stream closes — even on the exception path:
Path scratch = Files.createTempFile("work-", ".tmp");
try (BufferedWriter w = Files.newBufferedWriter(scratch,
StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
// ... write to and read from scratch ...
} // scratch is gone after this brace, regardless of how we got hereThe file is unlinked from the directory immediately on most Unix systems (other processes can no longer see it by name; only the handle keeps it alive). That's the right pattern for short-lived temp data — no try/finally plumbing to remember.
File.deleteOnExit() is the older, weaker version: it queues a delete to run during JVM shutdown. It's not called on kill -9 or a JVM crash, so it leaks. Use DELETE_ON_CLOSE when the file's lifetime is tied to a stream; use a try/finally (or try-with-resources around an AutoCloseable holder) when it isn't.
A note on "atomic replace"
Replacing one file with another atomically — so a reader never sees a half-written version — is not a delete-then-write pattern. The standard idiom is "write to a sibling, then atomically rename":
Path target = Path.of("data.json");
Path tmp = target.resolveSibling("data.json.tmp");
Files.writeString(tmp, payload);
Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);ATOMIC_MOVE swaps the two paths in a single OS step (on filesystems that support it). The old data.json is replaced; no in-between moment exists where the file is half written or missing.
A worked example: each deleter and a recursive tear-down
The program below builds a small tree, then exercises each deleter in turn — the legacy boolean, the modern throwing version, the "if exists" middle, and finally the Files.walk + reverseOrder pattern that removes a whole tree. Each step prints what happened.
What to take from the run:
- The two
File.delete()calls ona.txtreturnedtruethenfalse. The secondfalselooks identical to a permission failure — that's the legacy gap. Files.delete(sub)threwDirectoryNotEmptyExceptionandFiles.delete(missing)threwNoSuchFileException. Two specific subclasses ofIOException, two distinct failure modes — exactly what the boolean API can't tell you.Files.deleteIfExists(b)returnedtruethe first time andfalsethe second. That secondfalseis only "wasn't there" — a real failure (permission denied, lock) would have thrown.- The
Files.walk + reverseOrderblock deleted leaves first and parents last. EachFiles.deletecall along the way succeeded because, by the time the visitor reached a directory, its children had already been removed. - The
DELETE_ON_CLOSEfile existed inside thetryblock and was gone the moment the writer closed. That's the cleanest scratch-file pattern in the JDK — no shutdown hook, notry/finallyto remember.
What's next
That closes the high-level "do one thing to a file" chapters. The next chapter, Byte Streams in Java, drops one layer down: InputStream and OutputStream, the raw byte-oriented abstraction that every file, socket, and pipe in java.io is built on. Many of the helpers you've used so far — Files.readString, Files.newBufferedWriter, even FileReader — are decorators over those two interfaces.
Practice
`Files.deleteIfExists(path)` returns `false` when…