Java FileWriter overwrites logs on restart - append mode
FileWriter's default mode overwrites files silently on JVM restart, causing log loss.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- FileReader and FileWriter are character streams for reading/writing text files in Java.
- FileWriter overwrites by default; pass true for append mode to avoid silent data loss.
- Always wrap with BufferedReader/BufferedWriter — raw streams make one OS syscall per character.
- Platform default charset can corrupt non-ASCII data; use InputStreamReader/OutputStreamWriter with UTF-8.
- Always use try-with-resources to guarantee flush and close — missing close loses data silently.
- Use newLine() for platform-independent line separators, not hardcoded "\n".
FileWriter and FileReader are Java's basic character-stream classes for writing and reading text files. FileWriter writes characters to a file, and by default it creates a new file or truncates an existing one on every open — which is why your logs get wiped on restart.
The second constructor argument (FileWriter(file, true)) switches to append mode, preserving existing content and adding new data at the end. This is a common pitfall: forgetting that boolean flag means your application silently destroys previous output on each launch.
FileReader reads characters from a file, typically wrapped in a BufferedReader for line-by-line processing. These classes are part of java.io, the original I/O package, and they use the platform's default character encoding unless you specify otherwise.
For production systems, you'd often prefer java.nio.file.Files (which offers newBufferedWriter with explicit encoding and append options) or logging frameworks like Logback or Log4j that handle file rotation and append mode automatically. FileWriter/FileReader are fine for simple scripts or learning, but they lack control over encoding and error handling — expect corrupted data with non-ASCII characters if you don't explicitly set the charset via OutputStreamWriter wrapping a FileOutputStream.
The real-world lesson: always use the two-argument FileWriter constructor with true for logs, and never assume the default encoding matches your data.
Imagine your Java program is a person sitting at a desk. FileReader is like that person picking up a physical letter from a folder and reading it word by word. FileWriter is like that same person picking up a pen and writing a new letter into a folder. The 'file' on disk is the folder, and your Java program is the person doing the reading or writing. Simple as that.
Every serious application eventually needs to talk to the file system — reading config files, writing logs, importing CSV data, exporting reports. Java's FileReader and FileWriter are the most direct tools for doing exactly that with text files. They're part of the java.io package, which has been in Java since version 1.1, and understanding them properly unlocks a whole layer of practical programming that goes beyond printing to the console.
The problem they solve is straightforward: your program's memory is temporary. The moment your JVM shuts down, everything in RAM is gone. FileWriter lets you persist text data to disk so it survives restarts, reboots, and crashes. FileReader is the flip side — it lets you pull that saved data back into memory so your program can work with it again. Together they form the foundation of text-based file I/O in Java.
By the end of this article you'll know how FileReader and FileWriter work under the hood, how to use them safely with try-with-resources, how to append instead of overwrite, how to read files efficiently character by character or line by line, and exactly which real-world situations call for them versus their more powerful alternatives. You'll also know the three mistakes that trip up most intermediate developers — and how to avoid them entirely.
What FileWriter Actually Does — Writing Text to Disk
FileWriter is a character stream writer. That means it converts Java characters (which are Unicode) into bytes and writes them to a file. By default it uses the platform's default charset — more on why that matters in the pitfalls section.
When you create a FileWriter with just a filename, it opens the file in 'overwrite' mode. Every run wipes the file clean and starts fresh. If you pass true as the second argument, it switches to 'append' mode — new content goes to the end of the existing file. This is how log files work in most basic applications.
FileWriter extends OutputStreamWriter, which extends Writer. So it's fully polymorphic — anywhere you need a Writer, a FileWriter fits. This matters because it means you can wrap it with a BufferedWriter for dramatically better performance. Raw FileWriter hits the disk on every single call. BufferedWriter batches those writes into chunks. For anything longer than a few lines, always wrap.write()
You must close a FileWriter when you're done. Failing to do so is one of the most common bugs — the data never actually reaches the disk because it's still sitting in an internal buffer waiting to be flushed. The safest way to guarantee the file gets closed is try-with-resources, which Java handles automatically.
import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; public class WriteUserReport { public static void main(String[] args) { String reportFilePath = "user_report.txt"; // Try-with-resources ensures the writer is closed automatically, // even if an exception is thrown mid-write. // BufferedWriter wraps FileWriter for performance — without it, // every write() call is a separate OS-level disk operation. try (BufferedWriter reportWriter = new BufferedWriter(new FileWriter(reportFilePath))) { // Write the report header reportWriter.write("=== Monthly User Report ==="); reportWriter.newLine(); // platform-safe newline (\r\n on Windows, \n on Unix) reportWriter.write("Username: alice_dev"); reportWriter.newLine(); reportWriter.write("Logins this month: 47"); reportWriter.newLine(); reportWriter.write("Status: Active"); reportWriter.newLine(); // No need to call flush() or close() — try-with-resources does it System.out.println("Report written successfully to: " + reportFilePath); } catch (IOException writeException) { // IOException covers: file not found, permission denied, disk full, etc. System.err.println("Failed to write report: " + writeException.getMessage()); } } }
BufferedWriter.newLine() writes the correct line separator for whatever OS is running. Use it every time.write() call is a separate disk write.Appending to a File — The Second Argument That Changes Everything
The most common gotcha with FileWriter is accidentally nuking an existing file. If you're building a logger, an audit trail, or any kind of running history, you need append mode. The fix is a single boolean argument: new FileWriter(filePath, true). That true tells Java to open the file at the end rather than from the beginning.
Under the hood, true maps to the FileOutputStream append flag, which maps to the OS-level open call with O_APPEND. This means even if two processes try to append to the same file simultaneously, the OS handles the ordering — though for true concurrent logging in production you'd use a dedicated logging framework.
The pattern below simulates a simple application event log — each time the program runs, it adds a new timestamped entry without touching anything already in the file. This is exactly how application logs, audit trails, and event histories are built at a basic level.
import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class AppendEventLog { // Centralized log path — in a real app this comes from config private static final String LOG_FILE_PATH = "application_events.log"; private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public static void logEvent(String eventMessage) throws IOException { // The 'true' argument is the key — it enables append mode. // Without it, every call to logEvent() would destroy the previous log. try (BufferedWriter logWriter = new BufferedWriter(new FileWriter(LOG_FILE_PATH, true))) { String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); // Format: [2024-06-15 14:32:01] User alice_dev logged in logWriter.write("[" + timestamp + "] " + eventMessage); logWriter.newLine(); } // Writer is closed here — the OS commits this line to disk } public static void main(String[] args) throws IOException { // Simulate three events happening across one session logEvent("Application started"); logEvent("User alice_dev logged in"); logEvent("User alice_dev exported report"); System.out.println("Events logged. Check: " + LOG_FILE_PATH); } }
new FileWriter(path, true).new FileWriter(path, false) or default.Reading Files With FileReader — Character by Character and Line by Line
FileReader is the reading counterpart. Like FileWriter, it's a character stream — it reads bytes from disk and converts them to Java chars using the platform's default charset. Wrapping it with BufferedReader is not optional for real code. BufferedReader adds a read buffer (8KB by default) so Java isn't making a system call to the OS for every single character, and it provides the essential readLine() method.
readLine() returns the next line of text without the line terminator, or null when the file ends. That null check in the while loop is the idiomatic Java pattern for reading a file line by line. Forgetting that null signals EOF (end of file) and not an error is a classic beginner mistake.
The example below reads a simple CSV-style config file — the kind you'd use to store database connection settings or feature flags. It parses each line into a key-value pair, demonstrating a real use case rather than just printing raw file contents.
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class ReadConfigFile { // Assume this file exists on disk with the content shown in the output section private static final String CONFIG_FILE_PATH = "app_config.properties"; public static Map<String, String> loadConfig(String filePath) throws IOException { Map<String, String> configSettings = new HashMap<>(); // BufferedReader wraps FileReader — gives us readLine() and a memory buffer // so we're not hitting the disk character-by-character try (BufferedReader configReader = new BufferedReader(new FileReader(filePath))) { String currentLine; // readLine() returns null at end-of-file — NOT an empty string // This while loop is the standard Java pattern for reading all lines while ((currentLine = configReader.readLine()) != null) { // Skip blank lines and comment lines (lines starting with #) if (currentLine.isBlank() || currentLine.startsWith("#")) { continue; } // Each line is expected to look like: db.host=localhost String[] keyValuePair = currentLine.split("=", 2); if (keyValuePair.length == 2) { String settingKey = keyValuePair[0].trim(); String settingValue = keyValuePair[1].trim(); configSettings.put(settingKey, settingValue); } } } // BufferedReader (and the FileReader inside it) closed automatically here return configSettings; } public static void main(String[] args) { try { Map<String, String> appConfig = loadConfig(CONFIG_FILE_PATH); System.out.println("Loaded " + appConfig.size() + " config settings:"); appConfig.forEach((key, value) -> System.out.println(" " + key + " -> " + value) ); } catch (IOException configLoadException) { System.err.println("Could not load config: " + configLoadException.getMessage()); } } }
FileReader.read() makes one system call per character — that's potentially thousands of OS calls for a 10KB file. BufferedReader reads a chunk (8192 chars by default) into memory in one call, then serves your code from that buffer. This can be 10-100x faster on real hardware. This answer alone separates candidates who've actually used I/O from those who've just read about it.while ((line = reader.readLine()) != null) is correct, but many novices write while (!line.isEmpty()) and miss the last line.Files.readString() (Java 11+).Copying a Text File — Putting FileReader and FileWriter Together
The clearest way to understand both classes working together is to build a file copy utility. This is also a surprisingly common real-world task — think copying templates, creating backup files, or duplicating config files before modifying them.
This example reads from a source file line by line and writes each line to a destination file, preserving the structure. It also adds a metadata header to the copy — something a raw Files.copy() call couldn't do without extra steps.
Notice the try-with-resources block manages both the reader and the writer simultaneously. When the block exits — successfully or via exception — both streams are closed in reverse declaration order (writer first, then reader). This is Java's guaranteed cleanup contract, and it's the only safe way to handle multiple I/O resources together.
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.time.LocalDate; public class TextFileCopier { /** * Copies a text file from sourcePath to destinationPath, * prepending a metadata header to the copy. * * @param sourcePath path to the original text file * @param destinationPath path where the copy will be written * @throws IOException if either file cannot be opened or written */ public static void copyWithMetadata(String sourcePath, String destinationPath) throws IOException { // Both resources declared in one try-with-resources block — // Java closes them both automatically, in reverse order (writer, then reader) try ( BufferedReader sourceReader = new BufferedReader(new FileReader(sourcePath)); BufferedWriter destinationWriter = new BufferedWriter(new FileWriter(destinationPath)) ) { // Add a metadata header that doesn't exist in the original destinationWriter.write("# Copied from: " + sourcePath); destinationWriter.newLine(); destinationWriter.write("# Copy date: " + LocalDate.now()); destinationWriter.newLine(); destinationWriter.write("# ----------------------------------------"); destinationWriter.newLine(); String sourceLine; int lineCount = 0; // Read source line by line — null signals we've reached the end while ((sourceLine = sourceReader.readLine()) != null) { destinationWriter.write(sourceLine); destinationWriter.newLine(); lineCount++; } System.out.println("Copy complete. Lines copied: " + lineCount); System.out.println("Destination: " + destinationPath); } // Both streams flushed and closed here — content is safely on disk } public static void main(String[] args) { try { copyWithMetadata("original_template.txt", "template_backup_copy.txt"); } catch (IOException copyException) { System.err.println("File copy failed: " + copyException.getMessage()); } } }
try() parentheses. They close in reverse declaration order — always. This means if you have a reader feeding a writer, the writer closes first (flushing its buffer to disk) before the reader closes. Declare them in the order you open them and let Java handle the rest.Files.copy() (NIO) — simpler, faster, built-in buffering.Character Encoding Pitfalls — Why Your Non-ASCII Data Gets Corrupted
FileReader and FileWriter use the platform's default charset by default. On a US English Windows machine, that's usually windows-1252 or Cp1252. On a Linux server, it's often UTF-8. When you write a file with accented characters on your dev machine (windows-1252) and the file is read on a server (UTF-8), those characters become garbled — 'é' becomes 'é' or '?'.
This is the most invisible, hardest-to-debug bug in Java I/O because the code compiles, runs, and produces output — it's just the wrong output. No exception is thrown. The only way to detect it is to inspect the raw bytes or open the file on a different platform.
The fix is straightforward: never use raw FileReader/FileWriter when your content might contain non-ASCII characters. Instead, use InputStreamReader wrapping a FileInputStream, and OutputStreamWriter wrapping a FileOutputStream. Both accept an explicit charset parameter.
The example below shows how to write and read a file with UTF-8 encoding, guaranteeing the same result on any platform.
import java.io.*; import java.nio.charset.StandardCharsets; public class Utf8FileExample { public static void main(String[] args) throws IOException { String message = "Pièce de résistance — café au lait: €5,00 éñçödë"; String filePath = "utf8_demo.txt"; // Write using UTF-8 explicitly try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(filePath), StandardCharsets.UTF_8))) { writer.write(message); writer.newLine(); } // Read using UTF-8 explicitly try (BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(filePath), StandardCharsets.UTF_8))) { String line = reader.readLine(); System.out.println("Read back: " + line); System.out.println("Characters match: " + message.equals(line)); } } }
The Silent Resource Leak — Why FileReader/FileWriter Never Close Themselves
Every Java dev has written this: open a FileWriter, do some writes, forget to close. The file handle lingers. On Windows, the file stays locked. On Linux, you leak file descriptors until your app crashes with "Too many open files." The try-with-resources construct from Java 7 isn't optional—it's mandatory. Without it, you're gambling that your finally block always runs. In production, it won't. An exception in the write method skips your close call entirely. Buffered output remains in memory. Data vanishes. The JVM's finalizer might eventually close the stream, but that's non-deterministic and deprecated. Always declare your FileReader and FileWriter in the resource specification of a try-with-resources block. It calls close() automatically, even on exceptions. Your operating system will thank you.
import java.io.*; public class SafeFileCopy { public static void main(String[] args) throws IOException { File source = new File("input.txt"); File dest = new File("output.txt"); try (FileReader fr = new FileReader(source); FileWriter fw = new FileWriter(dest)) { int character; while ((character = fr.read()) != -1) { fw.write(character); } } // Both streams auto-closed here System.out.println("Copy complete. No file handles leaked."); } }
The Buffer Tax — Reading One Character at a Time Will Crumble Under Load
FileReader.read() returns a single character. FileWriter.write(int) writes a single character. Chaining these in a loop is the slowest possible I/O pattern. Each call hits the file system or disk. On a 10MB text file, that's 10 million system calls. A task that should take 150ms will take 15 seconds in production. The fix is absurdly simple: wrap them in BufferedReader and BufferedWriter. These add an internal 8KB buffer (default) so you read and write in chunks. For line-oriented data, use readLine() and write(String) directly. The performance improvement is often 100x or more. Never benchmark a single-file copy with raw readers and writers. Always buffer. If you need maximum throughput for binary data, skip character streams entirely and go with FileInputStream/FileOutputStream paired with BufferedInputStream.
import java.io.*; public class BufferedFileCopy { public static void main(String[] args) throws IOException { File input = new File("large.txt"); File output = new File("large_copy.txt"); long start = System.nanoTime(); try (BufferedReader reader = new BufferedReader(new FileReader(input)); BufferedWriter writer = new BufferedWriter(new FileWriter(output))) { String line; while ((line = reader.readLine()) != null) { writer.write(line); writer.newLine(); } } long elapsed = System.nanoTime() - start; System.out.printf("Copied %s in %.2f ms%n", input.getName(), elapsed / 1_000_000.0); } }
Production Incident: Log File Goes Missing After Server Restart
new FileWriter("transactions.log") without the second boolean parameter. This default mode overwrites the file every time the JVM starts. After the server restart, the old file contents were replaced with new data.new FileWriter("transactions.log", true) to enable append mode. Also added a health check that validates the file still contains expected entries after restart.- Always explicitly specify append mode (
true) for any persistent log, audit trail, or incremental output. - Treat file writer initialization as a configuration review item during release checklists.
- Add monitoring to detect sudden file truncation — e.g., compare expected line count with actual.
close() is in a finally block. Check disk space and permissions.new FileWriter(path) overwrites. Use new FileWriter(path, true) for append. Check if a file deletion occurs elsewhere.line != null, not !line.isEmpty().ls -la /path/to/file (Linux) or dir C:\path\to\file (Windows) — check file size and permissionscat /path/to/file (Linux) or type C:\path\to\file (Windows) — view file contents immediatelytry(). For manual close, add finally block.file -i /path/to/file (Linux) — shows charset of written fileCheck JVM default charset: System.out.println(Charset.defaultCharset())Inspect source code: search for `new FileWriter` and verify the boolean flag. If not present, add `true`.Run a quick test: create a writer with `true` and write a marker line, then restart app and verify marker still exists.new FileWriter(path, true). Also consider using StandardOpenOption.APPEND if using Files.newBufferedWriter.| Feature / Aspect | FileReader / FileWriter | Files.readString / Files.writeString (NIO) |
|---|---|---|
| Java version introduced | Java 1.1 | Java 11+ |
| Reading entire file | Requires loop + StringBuilder | One method call |
| Writing entire string | Requires open, write, close | One method call |
| Charset control | Only via InputStreamReader wrapper | Built-in Charset parameter |
| Best for | Streaming large files line by line | Small files, quick reads/writes |
| Buffering needed? | Yes — always wrap with Buffered* | Built in automatically |
| Append mode | new FileWriter(path, true) | StandardOpenOption.APPEND flag |
| Performance on large files | Excellent when buffered | Loads whole file into memory |
| Exception type | Checked IOException | Checked IOException |
| Closing resources | Must use try-with-resources | Handled internally |
Key takeaways
Files.readString() and Files.writeString() methods from Java NIO (11+)Common mistakes to avoid
4 patternsNot wrapping FileReader/FileWriter with their Buffered counterparts
Forgetting to close the stream (or not using try-with-resources)
writer.close() in a finally block, never just at the end of the try block where an exception could skip it.Relying on the platform default charset
Mistaking append mode for overwrite (or vice versa)
Interview Questions on This Topic
Why should you always wrap FileReader with BufferedReader rather than using FileReader directly? What exactly happens at the OS level if you don't?
FileReader.read() makes a system call to the OS for every single character. A 50KB file results in ~50,000 syscalls. BufferedReader reads an 8KB chunk into memory in one call, then serves subsequent reads from that buffer without touching the OS. This can be 10-100x faster and also provides the essential readLine() method.What is the difference between new FileWriter('log.txt') and new FileWriter('log.txt', true), and what real-world bug does confusing them cause?
FileReader and FileWriter use the platform's default charset. Why is this a problem, and how would you rewrite them to guarantee UTF-8 encoding in a cross-platform application?
Under what circumstances would you choose Files.readString() over FileReader, and vice versa?
Files.readString() (Java 11+) is ideal for small files (under ~10MB) you want to load entirely into memory — it's concise, charset-safe, and handles close automatically. Use FileReader with BufferedReader when processing large files line by line to avoid OutOfMemoryErrors, or when you need to apply custom parsing logic per line.Frequently Asked Questions
FileReader is the low-level stream that connects directly to a file on disk and reads one character at a time via OS system calls. BufferedReader is a wrapper that sits on top of FileReader and stores a chunk of characters in memory (8KB by default), dramatically reducing the number of OS calls. BufferedReader also adds the essential readLine() method. You almost always use both together: new BufferedReader(new FileReader(path)).
Yes — if the file doesn't exist, FileWriter creates it automatically. However, it will throw a FileNotFoundException (which is a subclass of IOException) if any parent directory in the path doesn't exist. So new FileWriter('reports/june/output.txt') will fail if the 'reports/june/' directory hasn't been created yet. Use new File('reports/june/').mkdirs() before writing if you need to guarantee the directory exists.
Use Files.readString() and Files.writeString() (available since Java 11) when dealing with small files you want to read or write in a single operation — it's simpler, handles charset properly, and requires less code. Use FileReader and FileWriter (wrapped in their Buffered counterparts) when streaming large files line by line, because NIO's single-call methods load the entire file into memory at once, which can cause OutOfMemoryErrors on multi-gigabyte files.
The internal buffer may never be flushed to disk, which means the data you think you wrote is actually still sitting in memory and is lost when the program ends. No exception is thrown. This is why try-with-resources is the standard approach — it guarantees close() (and hence flush()) is called even if an exception occurs.
No. FileReader and FileWriter are character streams designed for text. They attempt to decode bytes into characters using a charset, which corrupts binary data. For binary files (images, PDFs, serialized objects), use FileInputStream and FileOutputStream directly.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Java I/O. Mark it forged?
6 min read · try the examples if you haven't