skip to content

How does error reporting differ between File and Files, and how would you handle the case where a file might legitimately not exist?

level: middleimportance: should knowfreq 55%

answer

  1. File = boolean (fail-silent); Files = typed exceptions (fail-loud)
  2. NoSuchFile / AccessDenied / DirectoryNotEmpty / FileAlreadyExists
  3. deleteIfExists: false on absent, still throws on real errors
  4. avoid exists()+delete() (TOCTOU)

basics

~20 s

File methods return false when something goes wrong, so you don't know the reason. Files methods throw specific exceptions instead. If a file might legitimately be absent, use Files.deleteIfExists, which returns true/false without throwing for the missing case.

solid answer

~40 s

The core difference is fail-silent vs fail-loud. File.delete()/mkdir()/renameTo() return a boolean; on failure you just get false and lose the cause — missing file, permissions, locked, non-empty directory all look identical. Files instead throws typed IOExceptions: NoSuchFileException, AccessDeniedException, DirectoryNotEmptyException, FileAlreadyExistsException, so you can branch on the real problem. For the genuinely-optional case where absence is normal and not an error, NIO.2 provides convenience variants that return a boolean by design: Files.deleteIfExists(path) returns false if it wasn't there but still throws on real errors like permissions. That's the right tool — it distinguishes 'expected absence' from 'unexpected failure', which a bare File.delete() can never do.

go deeper

for a junior

Knows Files throws while File returns a boolean, and that deleteIfExists handles the optional-file case.

for a middle

Names several specific exception subtypes and picks delete vs deleteIfExists by whether absence is an error.

for a senior

Designs error handling that catches the specific cause, distinguishes expected absence from real failure, and avoids exists()+act races.

for a principal

Sets codebase conventions around fail-loud I/O, reasons about atomicity/TOCTOU and how silent failures hide data-loss bugs in production.

## The two failure philosophies **Fail-silent (File).** Legacy `java.io.File` mutating methods signal failure with a `boolean` return: ```java boolean deleted = file.delete(); // false = something went wrong, but WHAT? boolean made = dir.mkdir(); // false = couldn't create — why? boolean moved = a.renameTo(b); // false = move failed — why? ``` A `false` collapses *every* possible cause into one value: the path didn't exist, you lacked permission, the directory wasn't empty, the target already existed, a lock was held. The caller cannot recover intelligently and often just ignores the return value entirely — a classic source of silent data bugs. **Fail-loud (Files).** NIO.2's `Files` throws a *specific* subclass of `IOException` describing the cause: - `NoSuchFileException` — the path doesn't exist. - `AccessDeniedException` — permission denied. - `DirectoryNotEmptyException` — tried to delete a non-empty directory. - `FileAlreadyExistsException` — create/copy target already exists (without `REPLACE_EXISTING`). - `AtomicMoveNotSupportedException` — atomic move requested but the filesystem can't do it. Because these are distinct types, you can `catch` exactly the case you care about and let others propagate. ## Handling 'might legitimately be absent' Sometimes absence is **not** an error — e.g. deleting a temp file that may already be gone. Throwing `NoSuchFileException` there would force ugly try/catch. NIO.2 anticipates this with **boolean-returning convenience methods** that are *deliberately* lenient about the absence case while still throwing on real failures: ```java // Returns false if the file wasn't there (no exception), // but still THROWS AccessDeniedException / DirectoryNotEmptyException on real problems. boolean removed = Files.deleteIfExists(path); ``` Contrast the three options: | Need | Use | |---|---| | Absence is an error | `Files.delete(path)` (throws `NoSuchFileException`) | | Absence is fine, other failures aren't | `Files.deleteIfExists(path)` (returns boolean, throws on real errors) | | You truly don't care about any reason (legacy) | `file.delete()` (boolean, swallows everything) | The key insight: `deleteIfExists` is **not** the same as the old fail-silent `File.delete()`. It only forgives the *expected* absence; genuine errors still surface. That distinction — expected absence vs unexpected failure — is exactly what the old boolean API could never express. ## A note on checking-then-acting (TOCTOU) Avoid `if (Files.exists(p)) Files.delete(p);`. Between the check and the act the file can change (a **time-of-check-to-time-of-use** race), and you can still hit `NoSuchFileException`. `deleteIfExists` (or just delete-and-catch) does the check atomically inside one call and is the safer idiom.

  • Why is 'if Files.exists(p) then Files.delete(p)' discouraged?
    It's a time-of-check-to-time-of-use (TOCTOU) race: the file can be created or removed between the check and the delete, so you can still get NoSuchFileException or delete the wrong thing. Files.deleteIfExists does the check-and-act atomically.

saying these in an interview costs you the question

  • Treating Files.deleteIfExists as identical to the old fail-silent File.delete() — it still throws on permission/non-empty errors.
  • Using Files.exists() then Files.delete() (TOCTOU race) instead of an atomic call.
  • Catching generic IOException when you actually want to handle just NoSuchFileException differently.
  • Assuming File.delete() throws — it never does.

context