Skip to content
CalliCoder

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.