Skip to content
CalliCoder

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:

SituationexistsnotExists
The file is theretruefalse
The file is not therefalsetrue
Cannot tell — no permission on a parent directoryfalsefalse

!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.

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.

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.