📁 I/O, Files & Networking · Intermediate

File I/O exceptions in Java

IOException, NoSuchFileException, UncheckedIOException.

🧩 The mysteryYou try to read a list of files inside a stream with map(f -> Files.readString(...)). The compiler refuses. The method is fine, the lambda is fine… so what's the problem?

Expect failure

Disks fill up, files vanish, permissions change. That's why most I/O methods throw **IOException, a checked exception: the compiler forces you to catch it or declare throws**.

Specific subclasses

NIO.2 reports precise causes: **NoSuchFileException (path doesn't exist), AccessDeniedException (no permission), FileAlreadyExistsException** (e.g. Files.createFile on an existing file). Old java.io classes like FileInputStream throw **FileNotFoundException** instead.

🔮 Predict it

The missing file

Files.readString(Path.of("missing.txt")) runs and the file doesn't exist. What happens?

  1. It returns an empty String
  2. It returns null
  3. It throws NoSuchFileException
Show the answer

It throws **NoSuchFileException, a subclass of IOException. A missing file is an error the caller must handle**, not a valid empty value.

Lambdas can't throw checked exceptions

Function.apply declares no checked exceptions, so a lambda calling Files.readString inside map doesn't compile. The standard fix: catch the IOException and rethrow it wrapped in **UncheckedIOException**. Files.lines does the same for read errors that happen mid-stream.

files.stream()
    .map(f -> Files.readString(Path.of(f)))
    .toList(); // compile error

Fixing the lambda

✗ Hides the error
.map(f -> {
    try { return Files.readString(f); }
    catch (Exception e) { return null; }
})

Silently turns failures into nulls that explode later.

✓ Keeps the error
.map(f -> {
    try { return Files.readString(f); }
    catch (IOException e) {
        throw new UncheckedIOException(e);
    }
})

Unchecked, so the lambda compiles, and getCause() still holds the original IOException.

⚠️ The trap

The empty catch

catch (IOException e) { } makes failures invisible: the program carries on with missing data and nobody knows why. At minimum log it — better, rethrow or react to the specific subclass (create the missing file, alert on access denied).

try {
    config = Files.readString(p);
} catch (IOException e) {
    // swallowed: nobody will ever know
}
💼 In the real world

Errors that tell the truth

Good services react to specific I/O failures: create a missing cache directory, return 404 for a missing upload, page someone on AccessDenied. Swallowed IOExceptions are among the hardest production bugs to diagnose, because the logs show nothing.

Key takeaways

  1. IOException is checked: catch it or declare throws
  2. NIO.2 throws NoSuchFileException; old java.io throws FileNotFoundException
  3. Files.lines wraps read errors in UncheckedIOException
  4. Never swallow I/O errors with an empty catch
🤯 Did you know?

UncheckedIOException arrived in Java 8 together with streams — BufferedReader.lines() and Files.lines() use it to report read errors from inside a stream.

Practice questions

Files.readString(Path.of("missing.txt")) is called and the file doesn't exist. What happens?

  1. It returns an empty String
  2. It throws NoSuchFileException
  3. It throws FileNotFoundException
  4. It returns null
Check your answer

It throws NoSuchFileException. NIO.2 methods report a missing file with NoSuchFileException, a subclass of IOException. FileNotFoundException comes from older java.io classes like FileInputStream.

What does this print?

List<String> texts = Stream.of("a.txt", "b.txt")
    .map(f -> Files.readString(Path.of(f)))
    .toList();
System.out.println(texts);
  1. []
  2. Throws NoSuchFileException
  3. Compile error
  4. Throws UncheckedIOException
Check your answer

Compile error. readString throws the checked IOException, but Function.apply declares no checked exceptions, so the lambda doesn't compile.

Next: freezing a whole object into bytes — Java serialization, and why security teams fear it.