How to Check if a File or Directory Exists in Java
Published Updated Java 13 min read
Files.exists, Files.notExists and why they are not opposites, the race between checking and acting that makes the check pointless, and what a symbolic link does to the answer.
There are two ways to ask whether a file exists and a third question worth asking first: whether the
check is useful at all. Between the moment exists() returns true and the moment you open the
file, another process can delete it, so for most purposes the correct code does not check, it acts
and handles the failure.
Written against Java 17.
The NIO check
Path path = Path.of("/var/data/report.csv");
Files.exists(path); // true if it exists and is readable enough to tell
Files.notExists(path); // true if it definitely does not exist
Files.isRegularFile(path);
Files.isDirectory(path);
exists and notExists are not each other’s negation, and that is the point of having both.
Three outcomes are possible:
| Situation | exists | notExists |
|---|---|---|
| The file is there | true | false |
| The file is not there | false | true |
| Cannot tell — no permission on a parent directory | false | false |
!Files.exists(p) therefore means “absent or unknown”. When the distinction matters — deciding
whether to create something, deciding whether a configuration file was deliberately omitted —
Files.notExists(p) is the check that says what it means.
The legacy check
File file = new File("/var/data/report.csv");
file.exists();
file.isFile();
file.isDirectory();
java.io.File collapses all three outcomes into false. It also gives no way to distinguish “does
not exist” from “the disk is unreachable”, and its methods report failure by returning false
without ever saying why.
That is the general reason to prefer java.nio.file: NIO throws IOException with a message and a
path, where File returns a boolean. For an existence check specifically, the difference is the
third state above.
Symbolic links
By default the check follows links, so a link pointing at a deleted target reports as absent:
Files.exists(link); // false — the target is gone
Files.exists(link, LinkOption.NOFOLLOW_LINKS); // true — the link itself is there
Both answers are correct for different questions. “Can I read this file” follows the link; “is there an entry with this name” does not. A cleanup routine that deletes broken symlinks needs the second, and written with the default it deletes nothing and reports nothing wrong.
Files.isSymbolicLink(path) never follows, by definition, and returns false for a path that does
not exist at all — so it answers “is this a link”, not “is this not a regular file”.
The race, and why the check is usually the wrong tool
if (!Files.exists(path)) {
Files.createFile(path); // another process may have created it by now
}
Between the two statements the state can change. This is a time-of-check to time-of-use race, and in a security-sensitive context — a temp directory writable by other users — it is an exploitable one: the attacker creates a symlink at that path in the gap, and your write lands wherever it points.
The fix is not a smaller gap. It is an operation that decides and acts in one step, which the filesystem can do atomically and your code cannot:
try {
Files.createFile(path); // fails if it already exists
} catch (FileAlreadyExistsException e) {
// handle the collision
}
try (var out = Files.newBufferedWriter(path, StandardOpenOption.CREATE_NEW)) {
out.write(content);
}
Same shape for reading — do not check, open:
try (var in = Files.newBufferedReader(path)) {
// ...
} catch (NoSuchFileException e) {
// the file is genuinely absent
} catch (AccessDeniedException e) {
// it exists and you cannot read it — a distinction exists() cannot make
}
NoSuchFileException and AccessDeniedException are both subclasses of IOException, and catching
them separately gives you a diagnosis that no combination of boolean checks can produce.
When checking is legitimate
Three cases, all of them advisory rather than protective:
Reporting. A start-up message that says which optional configuration files were found. Nothing depends on the answer staying true.
Choosing a path. Picking the first of several candidate locations that exists. A wrong answer degrades into the normal not-found handling.
Validation before expensive work. Rejecting a batch job early because its input directory is missing, rather than failing after ten minutes. The real check is still the open.
In all three the check improves the message, not the correctness. A useful way to keep that honest is to write the fallback first: if the code has no sensible behaviour for “the check said yes and the open still failed”, the check is load-bearing and should be removed in favour of the open.
Paths that do not mean what they read
Two path values can name the same file and compare unequal, which turns an existence check into a question about text rather than about the filesystem:
Path a = Path.of("/var/data/../data/report.csv");
Path b = Path.of("/var/data/report.csv");
a.equals(b); // false — Path equality is lexical
Files.isSameFile(a, b); // true — asks the filesystem
a.normalize().equals(b); // true — removes . and ..
a.toRealPath().equals(b); // true — also resolves symlinks, throws if absent
Path.equals compares the components as text, with case sensitivity following the platform. It never
touches the disk, so two paths to one file are unequal and a path with a trailing separator may be
unequal to the same path without one.
toRealPath() is the canonical form: absolute, normalised, symlinks resolved. It throws
NoSuchFileException when the file is absent, which makes it an existence check with a useful return
value — and the right tool when a path came from outside the application and has to be confined to a
directory:
Path base = Path.of("/var/data").toRealPath();
Path requested = base.resolve(userSuppliedName).normalize();
if (!requested.startsWith(base)) {
throw new IllegalArgumentException("Path escapes the base directory");
}
Without the normalize() and the startsWith, a name containing ../../etc/passwd resolves outside
the directory and every existence check on it answers honestly about the wrong file.
Checking more than existence
Files.isReadable(path);
Files.isWritable(path);
Files.isExecutable(path);
Files.size(path); // throws if absent — no boolean
isReadable has exactly the same race and a second problem: on some filesystems and with some
security policies, the permission bits do not determine the outcome of an open. Treat it as advisory
and let the open decide.
Reading several attributes at once is worth knowing about, because each individual call is a separate system call:
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.isRegularFile();
attrs.isDirectory();
attrs.size();
attrs.lastModifiedTime();
One call, one consistent snapshot. Files.exists followed by Files.size followed by
Files.getLastModifiedTime is three calls that can each see a different state.
Directories, and creating them safely
Files.createDirectories(path); // creates missing parents, succeeds if it already exists
Files.createDirectory(path); // fails if the parent is missing or it already exists
createDirectories is idempotent by design — it does not throw FileAlreadyExistsException for an
existing directory — so the “check then create” pattern has no reason to exist for directories at
all.
Checking whether a directory is empty needs a stream rather than a listing, so that a directory with a million entries does not build a million-element list:
try (Stream<Path> entries = Files.list(dir)) {
boolean empty = entries.findFirst().isEmpty();
}
The try-with-resources is required. Files.list, Files.walk and Files.find hold an open directory
handle, and leaking them exhausts the file-descriptor limit — a failure that appears much later, in
unrelated code, as “too many open files”.
Related: creating a new file and temporary files. More in the Java guides.
Frequently asked questions
What is the difference between Files.exists and Files.notExists?
They are not opposites. Both
return false when the answer cannot be determined — typically no permission on a parent directory —
so notExists is the right check when “definitely absent” is what you need.
File.exists() or Files.exists()?
Files.exists. The NIO API distinguishes the unknown case and
throws informative exceptions elsewhere; java.io.File returns false for every kind of failure.
Why is checking before creating a file unsafe?
The state can change between the check and the
create. Use Files.createFile or StandardOpenOption.CREATE_NEW and catch
FileAlreadyExistsException — the filesystem decides atomically.
Does Files.exists follow symbolic links?
Yes, by default, so a link with a deleted target reports
as absent. Pass LinkOption.NOFOLLOW_LINKS to ask about the link itself.
How do I tell whether a path is a file or a directory?
Files.isRegularFile and
Files.isDirectory, or read BasicFileAttributes once and ask it both questions from one snapshot.
Is Files.isReadable reliable?
Only as a hint. It has the same race, and permission bits do not
always determine whether an open succeeds. Open the file and catch AccessDeniedException.
How do I check whether a directory is empty?
Open a Files.list stream in try-with-resources and
call findFirst().isEmpty(). Do not collect the entries — a large directory makes that expensive for
no reason.
Do I need to close Files.list?
Yes. It holds a directory handle, and leaking it eventually exhausts the process’s file descriptors, which surfaces as an unrelated failure elsewhere.
How do I create a directory only if it does not exist?
Files.createDirectories. It creates
missing parents and succeeds silently when the directory is already there, so no check is needed.
Which exception means the file was missing?
NoSuchFileException. AccessDeniedException means
it exists and you could not open it — a distinction no boolean check can make.