Closing the Loop: Java FileReader close() Done Right, with Real Examples

The first time I saw a production incident caused by a missing close(), it looked harmless: a tiny service that read a handful of text files per request. The bug was subtle—file descriptors slowly piled up, then the service hit the OS limit and started failing unrelated operations. That’s when I learned to treat FileReader.close() as a first‑class part of the API, not an afterthought. If you read files with FileReader, you’re borrowing system resources, and you owe the system a clean return.

In this post, I’ll walk you through what close() actually does, why it exists as an abstract method, and how it behaves in real JVM workloads. I’ll show full, runnable examples, highlight failure cases that surprise even experienced developers, and map classic patterns to modern 2026 workflows like structured logging, observability, and AI‑assisted debugging. By the end, you’ll know exactly when to call close(), how to guarantee it happens, and how to avoid the most common traps that show up in code reviews.

Why close() matters more than you think

When you open a FileReader, you’re not just getting a Java object. You’re opening a low‑level OS file handle. That handle consumes limited resources: file descriptors, kernel buffers, and sometimes locks on the file itself. Close() is the official signal that you’re done. Once you call it, the underlying stream is closed and the JVM asks the OS to release all the associated resources.

I like to think of FileReader as a library book. You can read it, mark a page, or reset to a previous point. But if you keep the book forever, no one else gets it, and the library eventually runs out of copies. Close() is the moment you return the book to the shelf.

There are two practical impacts to understand:

1) Resource pressure: File handles are scarce. On many systems, per‑process limits are in the thousands. A leak might take minutes or hours to show up, which makes it hard to trace.

2) Behavior after close(): Once you call close(), any read(), ready(), mark(), reset(), or skip() on that FileReader will throw IOException. You should treat the stream as dead.

If you rely on the reader after close(), you’re asking the JVM to read from something that no longer exists. This is not a “maybe” error; it’s a hard failure.

The close() contract in one sentence

The FileReader close() method closes the stream and releases all associated system resources. After close() is called, any attempt to read from the reader will throw IOException.

That’s the practical contract. It’s short, but it carries a lot of weight. FileReader itself is a concrete subclass of InputStreamReader, which inherits close() as an abstract method from Reader. That’s why the signature appears as:

public abstract void close()

The method takes no parameters and returns no value. The absence of parameters is a hint: you can’t “partially” close a reader. It’s a binary state—open or closed.

The simplest working example

When I teach juniors, I start with a straight‑line example: open a file, read it, close it. Here’s a complete program you can run locally.

import java.io.FileReader;

import java.io.IOException;

public class FileReaderCloseDemo {

public static void main(String[] args) {

String path = "C:/data/notes/today.txt";

try {

FileReader reader = new FileReader(path);

int ch;

while ((ch = reader.read()) != -1) {

System.out.print((char) ch);

}

reader.close();

} catch (IOException ex) {

System.out.println("File error: " + ex.getMessage());

}

}

}

Key points:

  • The loop reads characters until read() returns -1, the end‑of‑file marker.
  • After the loop, close() is called exactly once.
  • Any exception is caught and logged. In real projects, I recommend structured logging with a trace id so you can correlate logs with user requests.

This is the essential shape. It’s not fancy, but it’s correct.

What happens if you read after close()

Here’s the failure case I see most in code reviews: a developer closes the reader early and then tries to continue reading from it. The JVM immediately throws an IOException. Try it once and you’ll never forget.

import java.io.FileReader;

import java.io.IOException;

public class ReadAfterCloseDemo {

public static void main(String[] args) {

String path = "C:/data/notes/today.txt";

try {

FileReader reader = new FileReader(path);

reader.close();

int ch;

while ((ch = reader.read()) != -1) {

System.out.print((char) ch);

}

} catch (IOException ex) {

System.out.println("Error: " + ex.getMessage());

}

}

}

When you run this, you’ll get an exception message like:

Error: Stream closed

The exception message can vary by JDK, but the root cause is always the same. Once the stream is closed, the reader is no longer valid. If you need to read the file again, you must open a new FileReader.

Guaranteeing close() with try-with-resources

I recommend try-with-resources as the default approach in 2026. It’s cleaner, safer, and easier to review. It also allows the compiler to ensure resources are closed even if an exception interrupts the flow.

Here’s the same example rewritten with try‑with‑resources:

import java.io.FileReader;

import java.io.IOException;

public class TryWithResourcesDemo {

public static void main(String[] args) {

String path = "C:/data/notes/today.txt";

try (FileReader reader = new FileReader(path)) {

int ch;

while ((ch = reader.read()) != -1) {

System.out.print((char) ch);

}

} catch (IOException ex) {

System.out.println("File error: " + ex.getMessage());

}

}

}

No explicit close() call, but close() still happens—automatically and reliably. This is the most common style I use in production code.

Why I trust try-with-resources

In modern Java, auto‑closing is both idiomatic and safer. It handles exceptions in two places: inside the try block, and during close() itself. If close() throws an exception, it becomes a suppressed exception. That matters when you’re diagnosing issues in logs or AI‑assisted debugging sessions.

If you use IntelliJ or other 2026 IDEs, you’ll see warnings if you forget to close a FileReader. That’s good tooling, but you shouldn’t rely solely on warnings—build the pattern into your habits.

Common mistakes I see in reviews

Even experienced developers trip on close() in subtle ways. Here are the most frequent ones I flag, plus the fix I recommend.

1) Closing in the wrong place

Bad pattern:

FileReader reader = new FileReader(path);

reader.close();

int ch = reader.read();

Fix: Close only after you’re done reading, or switch to try‑with‑resources so you can’t get it wrong.

2) Forgetting to close in the exception path

Bad pattern:

FileReader reader = new FileReader(path);

int ch = reader.read();

// exception happens here

reader.close();

Fix: Wrap it in try‑with‑resources, or use a finally block:

FileReader reader = null;

try {

reader = new FileReader(path);

int ch = reader.read();

} finally {

if (reader != null) reader.close();

}

3) Swallowing exceptions from close()

If close() throws, the cause matters. If you ignore it, you can hide a real failure. With try‑with‑resources, the JVM handles it more safely by attaching it as a suppressed exception.

4) Closing shared readers

In legacy systems, I still see a FileReader shared between methods. If one method closes it, the rest of the code breaks. If you must share a reader, define clear ownership: the method that creates it is responsible for closing it, and no one else should call close(). Better yet, avoid shared FileReader instances and pass data instead.

5) Reading char‑by‑char in high‑volume loops

This is not a close() bug, but it’s a performance footgun I often address at the same time. Reading one character at a time can be fine for small files, but it can be slow for large files. Consider buffering with BufferedReader when you read many characters.

Real-world scenario: processing log files safely

Let’s say you run a small ETL job that reads application logs and extracts error lines. This is a typical task for microservices teams and SRE workflows. You open each file, read it line by line, then close the reader.

Here’s a runnable example with modern logging conventions and a clear close path:

import java.io.BufferedReader;

import java.io.FileReader;

import java.io.IOException;

public class LogScanner {

public static void main(String[] args) {

String logPath = "C:/logs/order-service-2026-02-17.log";

try (BufferedReader reader = new BufferedReader(new FileReader(logPath))) {

String line;

while ((line = reader.readLine()) != null) {

if (line.contains("ERROR")) {

System.out.println("Found error: " + line);

}

}

} catch (IOException ex) {

System.out.println("Failed to scan log: " + ex.getMessage());

}

}

}

Why I like this example:

  • It uses BufferedReader for efficiency.
  • It still relies on FileReader under the hood, so close() is essential.
  • The try‑with‑resources ensures everything closes even if readLine() throws.

If you remove try‑with‑resources and forget to call close(), this job might keep file handles open for every log file it scans. In a long‑running service, that’s a real production risk.

When close() becomes a performance issue

Close() itself is fast. Typically, it takes microseconds to a couple of milliseconds depending on OS and filesystem. But there are cases where close() can appear slow:

  • Network filesystems: closing a reader on a remote file can wait for the network to flush.
  • High contention: in environments with many concurrent file operations, the OS can delay release.
  • Antivirus or file watchers: on Windows, close() can trigger scanning or indexing.

I mention this because developers sometimes mistake a slow close() for a bug in their code. In real systems, I track close() timing with telemetry only when there’s a proven performance issue. For most workloads, it’s a non‑issue.

When to use FileReader—and when not to

FileReader is simple and direct, which makes it perfect for small text files and quick scripts. But I don’t use it everywhere.

Use FileReader when:

  • The file is plain text.
  • The encoding is compatible with the platform default (or you control the environment).
  • You want lightweight API surface.

Avoid FileReader when:

  • You must control the character encoding explicitly (use InputStreamReader with a charset).
  • You’re reading large files in high throughput scenarios (use BufferedReader or Files.newBufferedReader).
  • You’re reading binary data (use FileInputStream instead).

If you need explicit encoding, I usually write:

import java.io.BufferedReader;

import java.io.FileInputStream;

import java.io.InputStreamReader;

import java.nio.charset.StandardCharsets;

try (BufferedReader reader = new BufferedReader(

new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {

// read text

}

Close() still matters here because InputStreamReader and FileInputStream both rely on OS resources.

A simple analogy that sticks

I often compare close() to turning off the water after you fill a glass. The faucet gives you what you need, but you close it so water doesn’t keep running. If you walk away without closing it, the cost shows up later. File handles are similar: small in isolation, expensive in aggregate.

That’s the mental model I want you to keep. Close() isn’t a boilerplate chore. It’s an explicit handshake with the system.

Modern practice: auto‑close, static analysis, and AI helpers

In 2026, most teams don’t rely on memory alone to manage close(). They use tooling:

  • Static analysis: IDEs and linters warn when streams aren’t closed.
  • Build gates: code review bots can reject PRs that leak resources.
  • AI code review: modern copilots flag missing close() calls and propose try‑with‑resources automatically.

I still recommend understanding the behavior yourself. Tooling is helpful, but not a replacement for knowledge. When you know why close() matters, you’ll spot issues early and fix them quickly.

Traditional vs modern patterns (quick comparison)

Here’s the table I use to explain the shift in style to newer engineers.

Pattern

Traditional approach

Modern 2026 approach

My recommendation

Closing streams

Manual close() in finally

try‑with‑resources

Always use try‑with‑resources unless you have a strong reason not to

Logging errors

System.out.println

Structured logging with context

Use structured logs for production, plain output for demos

Reading text

FileReader char‑by‑char

BufferedReader line‑by‑line

For non‑tiny files, use BufferedReader

Error handling

Catch Exception

Catch IOException

Be precise and log enough detailNotice that the modern approach doesn’t remove close()—it just makes it harder to forget.

Edge cases you should think about

1) Multi‑threaded access

FileReader is not thread‑safe. If multiple threads read the same FileReader and one thread closes it, others will crash. If you need multi‑threaded reads, create separate readers per thread or use a different design.

2) Short‑circuit returns

If your method can return early, you can leak resources. Try‑with‑resources solves this cleanly.

3) Exception during close()

If close() itself fails, it usually means an I/O issue. In try‑with‑resources, close() exceptions are captured and attached as suppressed exceptions. You should make sure your logging includes suppressed exceptions, or you’ll miss vital context.

4) Relying on finalize()

Modern JVMs discourage finalizers. You might hear that garbage collection will eventually close the file. That’s not reliable. File handles are scarce, and you should close them explicitly.

5) Ignoring character encoding

FileReader uses the platform default encoding. That can be OK in tightly controlled environments, but it’s risky in cross‑platform software. If you read a UTF‑8 file on a system with a different default encoding, you can corrupt data or get unexpected characters. In those cases, use InputStreamReader with a defined charset and close it as usual.

Practical guide: a safe template I reuse

Here’s a template I reuse when I need a quick, safe FileReader pattern. You can copy it as‑is and drop it into your project.

import java.io.BufferedReader;

import java.io.FileReader;

import java.io.IOException;

public class SafeReaderTemplate {

public static void main(String[] args) {

String path = "C:/data/notes/today.txt";

try (BufferedReader reader = new BufferedReader(new FileReader(path))) {

String line;

while ((line = reader.readLine()) != null) {

// Process each line

System.out.println(line);

}

} catch (IOException ex) {

System.out.println("Read failed: " + ex.getMessage());

}

}

}

This template scales well and keeps close() behavior correct by construction.

Why close() is still relevant in modern Java

Even with higher‑level APIs like Files.readString or Files.lines, close() still matters. Those APIs open resources under the hood, and they still need to be closed. Some methods handle it automatically, but others return lazy streams that require you to close them.

For example, Files.lines(Path) returns a Stream that must be closed, usually with try‑with‑resources. If you forget, you leak file handles just as you would with FileReader.

So, while you might use FileReader less often in 2026, the core idea behind close() is still central to Java I/O.

A short checklist for code reviews

When I review code that uses FileReader, I quickly scan for these items:

  • Is close() guaranteed to run on every path?
  • Is try‑with‑resources used where possible?
  • Is FileReader the right choice given encoding and size?
  • Are errors handled with enough context?
  • Is the reader shared across methods or threads?

If any answer is no, I leave a comment. This checklist keeps review time short and outcomes predictable.

Key takeaways and your next steps

When you work with FileReader, you’re handling real system resources. Close() is the handshake that returns those resources back to the OS. If you forget, your code might pass tests but fail under real load—often hours later when you least expect it.

Here’s how I recommend you move forward:

  • Use try‑with‑resources by default. It’s the most reliable way to guarantee close().
  • Treat FileReader as a short‑lived object. Create it, read your data, close it. Don’t share it across threads or methods unless you have a clear ownership model.
  • Be explicit about character encoding if your data is not guaranteed to match the platform default.
  • In code reviews, look for early returns, exception paths, and hidden loops that bypass close().
  • If you’re building services that read many files, add telemetry to detect resource pressure early.

In my experience, the developers who learn to respect close() early end up writing more reliable and calmer code. It’s not glamorous, but it’s the difference between a stable system and a slow‑moving incident. If you’re touching file I/O today, spend five extra minutes to make close() bulletproof—you’ll save yourself hours later.

Scroll to Top