How to Create a New File in Java
Java 13 min read
Files.createFile against File.createNewFile, the parent directory that has to exist first, POSIX permissions set at creation rather than after, and the temp-and-move pattern that makes a write atomic.
Creating a file is one call in two APIs, and the difference between them is what happens when it
fails. One throws an exception naming the path and the reason; the other returns false. For a
half-second operation that can fail for six different reasons, that distinction is the whole
argument.
Written against Java 17.
The NIO way
Path path = Path.of("/var/data/report.csv");
try {
Files.createFile(path);
} catch (FileAlreadyExistsException e) {
// it was already there
} catch (NoSuchFileException e) {
// the parent directory does not exist
} catch (AccessDeniedException e) {
// no permission
}
Files.createFile creates an empty file atomically, the check for existence and the creation are
one operation the filesystem performs, so two threads or two processes racing produce exactly one
success and one FileAlreadyExistsException.
That atomicity is the reason to prefer it over checking first. if (!Files.exists(p)) Files.createFile(p) has a gap between the two statements, and in a shared directory the gap is
exploitable.
It does not create parent directories. A missing parent is NoSuchFileException, which reads
oddly for a call whose job is to create something:
Files.createDirectories(path.getParent()); // idempotent — no check needed
Files.createFile(path);
The legacy way
File file = new File("/var/data/report.csv");
boolean created = file.createNewFile(); // false if it already existed
Also atomic, and it returns a boolean where the NIO version throws. The problem is everything else:
false means “already existed”, while a missing parent or a permission failure throws a bare
IOException whose message is platform-dependent. Distinguishing the cases means parsing a string.
file.mkdirs() is the parent-creating equivalent of createDirectories, with the same boolean
reporting.
Use java.io.File only where an API forces it. path.toFile() and file.toPath() convert in either
direction, and the conversion is cheap enough to do at the boundary of every legacy call rather than
letting File spread inward.
Creating a file with content
Creating an empty file and then opening it is two operations where one will do:
Files.writeString(path, "id,name\n1,first\n", StandardCharsets.UTF_8);
Files.write(path, bytes);
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("id,name");
writer.newLine();
}
Always name the charset. Files.writeString and newBufferedWriter default to UTF-8, but
FileWriter, String.getBytes() and InputStreamReader historically default to the platform
charset, which differs between a developer’s machine and a container. Java 18 changed the file.encoding
default to UTF-8, and code that has to run on 17 or earlier cannot rely on it.
The open options control what happens when the file is already there:
Files.newBufferedWriter(path, StandardOpenOption.CREATE_NEW); // fail if it exists
Files.newBufferedWriter(path, StandardOpenOption.CREATE); // create or truncate (default)
Files.newBufferedWriter(path, StandardOpenOption.APPEND); // create or append
CREATE_NEW is the one to reach for when the file’s absence is part of the contract, a lock file, a
one-shot export. It is the same atomic guarantee as createFile, with the content written in the
same breath.
The default set (CREATE, WRITE, TRUNCATE_EXISTING) silently destroys an existing file, which
is usually intended and occasionally catastrophic.
Permissions at creation, not after
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
FileAttribute<?> attr = PosixFilePermissions.asFileAttribute(perms);
Files.createFile(path, attr);
Passing the attribute to createFile sets the permissions as the file is created. Creating it and
then calling Files.setPosixFilePermissions leaves a window in which the file exists with the
default permissions, and for anything containing a credential or personal data that window is the
vulnerability.
The umask still applies to the default case, which is why a file created without the attribute is
typically rw-r--r--, readable by everyone on the machine.
POSIX permissions do not exist on Windows, where the call throws UnsupportedOperationException.
Guard it if the code has to run on both:
if (path.getFileSystem().supportedFileAttributeViews().contains("posix")) {
Files.createFile(path, attr);
} else {
Files.createFile(path);
}
Writing atomically
A process killed halfway through a write leaves a truncated file, and any reader that arrives in the meantime sees a partial one. Neither is acceptable for a configuration file or an export that something else polls.
The pattern is to write elsewhere and rename:
Path target = Path.of("/var/data/report.csv");
Path temp = Files.createTempFile(target.getParent(), "report", ".tmp");
try {
Files.writeString(temp, content, StandardCharsets.UTF_8);
Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
Files.deleteIfExists(temp);
throw e;
}
Two details make it work. The temporary file must be on the same filesystem as the target —
hence creating it in the target’s own directory rather than in /tmp, because a move across
filesystems is a copy plus a delete and is not atomic. And ATOMIC_MOVE throws
AtomicMoveNotSupportedException rather than silently degrading, which is the behaviour you want.
Readers then see either the old file or the new one, never a partial write. For durability across a
power loss the file also needs an fsync before the move, which in Java means opening with
StandardOpenOption.SYNC or calling FileChannel.force(true).
Strictly, full durability needs the directory synced as well, because the rename itself is metadata that can be lost even when the file’s contents are on disk. The JDK has no portable way to do that, which is one of the reasons databases do their own file handling rather than using this pattern.
For most applications the distinction is academic: the point of the temp-and-move is that no reader ever observes a half-written file, and that guarantee holds without the sync. The sync is about surviving a machine losing power at the wrong microsecond, which is a different requirement and worth being explicit about which one you have.
Choosing where the file goes
A hard-coded absolute path is portable until it is not. Three sources are worth knowing:
Path working = Path.of("").toAbsolutePath(); // the process working directory
Path home = Path.of(System.getProperty("user.home"));
Path temp = Path.of(System.getProperty("java.io.tmpdir"));
A relative Path.of("report.csv") resolves against the working directory, which is set by whoever
started the process rather than by the code. That makes it fine for a command-line tool and wrong for
a service, where the working directory can be / and often is.
Building a path from segments rather than concatenating strings avoids the separator question entirely:
Path path = home.resolve("data").resolve("2026").resolve("report.csv");
resolve uses the platform separator and handles the case where the argument is already absolute, in
which case it replaces rather than appends, worth knowing before passing user input to it.
Deleting, for completeness
Files.delete(path); // throws NoSuchFileException if absent
Files.deleteIfExists(path); // returns false instead
The second is the one for cleanup code, and it has the usual race caveat: false means “not there
when we looked”. Deleting a directory requires it to be empty, so recursive deletion is a walk:
try (Stream<Path> walk = Files.walk(dir)) {
walk.sorted(Comparator.reverseOrder()).forEach(p -> {
try { Files.delete(p); } catch (IOException e) { throw new UncheckedIOException(e); }
});
}
The reverse ordering is what puts children before their parents. There is still no recursive delete in the JDK, which is a deliberate omission rather than an oversight.
Creating a file in a directory that may not exist
Putting it together, the common case is three lines:
Path path = Path.of("/var/data/2026/08/report.csv");
Files.createDirectories(path.getParent());
Files.writeString(path, content, StandardCharsets.UTF_8);
No existence check anywhere. createDirectories is idempotent, writeString creates or truncates,
and every failure arrives as a typed exception naming the path.
Related: checking whether a file exists and temporary files. More in the Java guides.
Frequently asked questions
Files.createFile or File.createNewFile?
Files.createFile. Both are atomic; the NIO version
throws a typed exception naming the path and the reason, where the legacy one returns false for one
failure and a generic IOException for the rest.
Does Files.createFile create parent directories?
No. A missing parent is NoSuchFileException.
Call Files.createDirectories(path.getParent()) first. It is idempotent, so no check is needed.
How do I create a file only if it does not already exist?
Files.createFile, or
StandardOpenOption.CREATE_NEW when writing content at the same time. Both fail with
FileAlreadyExistsException rather than overwriting.
Why should I not check exists() before creating?
The state can change between the check and the create, and in a shared directory that gap is exploitable. Let the atomic call decide and catch the exception.
What is the default behaviour when the file already exists?
Files.write and writeString
truncate it. That is CREATE, WRITE, TRUNCATE_EXISTING, the implicit option set.
Do I need to specify a charset?
Yes, unless you are certain of the platform. NIO’s writers default
to UTF-8, but FileWriter and String.getBytes() historically use the platform default, which
differs between machines.
How do I create a file with restricted permissions?
Pass a FileAttribute built from
PosixFilePermissions to Files.createFile. Setting permissions after creation leaves a window in
which the file is world-readable.
Does that work on Windows?
No, it throws UnsupportedOperationException. Check
supportedFileAttributeViews().contains("posix") if the code must run on both.
How do I make a file write atomic?
Write to a temporary file in the same directory, then
Files.move with REPLACE_EXISTING and ATOMIC_MOVE. Same filesystem is required, or the move is a
copy and a delete.
Why did my atomic move throw?
AtomicMoveNotSupportedException means the source and target are
on different filesystems. Create the temporary file in the target’s own directory.