← Java: language and application decisions
06 / 10 · 45 MIN

Exceptions, resources and files

Preserve useful failures, manage resources and distinguish paths, reads and publication guarantees.

Failure, absence and valid results

A read may return empty text because the file is valid and empty, or it may fail before obtaining a result. Returning the same empty String in both cases removes a distinction the caller may need. In a configuration service, empty content might mean disabling rules; an access failure must not accidentally cause that decision.

IOException is checked. A method calling Files.readString must handle that failure or declare it in its contract. Adding throws IOException permits propagation; it does not guarantee reading will succeed. RuntimeException and Error have no equivalent declaration requirement, although they can still interrupt operations.

Choose handling boundaries by responsibility: the reading layer may add context while preserving the cause, and the layer managing active versions decides whether to retain previous configuration. new IOException("Configuration read failed", cause) preserves the original error relationship. Avoid logging sensitive contents or treating exception text as configuration data. Examples assume the API imports shown in the snippet.

Handlers and exits that hide failures

Catch clauses are considered in written order. If IOException precedes FileNotFoundException, the general handler already covers that subtype and compilation rejects the ordering. Place specific handling before general handling when the application needs different decisions. Deciding to retry should not depend only on whether an exception class sounds recoverable: establish effects already produced and the operation contract.

finally performs exit work when ordinary language control leaves the try, but it can also replace the earlier outcome. If try throws IllegalStateException and finally returns 7, the call returns 7 and no longer propagates that exception. A cleanup error thrown from a manual finally may likewise hide the original failure.

A diagnostic policy should avoid success returns in finally. Keep the appropriate cleanup responsibility there and preserve the operation’s result or exception. This is not a promise of execution during abrupt process termination or power loss. Exercises analyze ordinary JVM control flow and explicitly stated failures.

Acquisition, closing and suppressed exceptions

try-with-resources declares resources whose exit must be managed. Initializers are evaluated left to right; non-null resources close in reverse order. Opening A and then B leads to closing B then A. A B closing failure does not prevent attempting to close A.

If the body has already thrown an exception, it remains primary; later closing failures are added as suppressed exceptions with normal suppression enabled. getSuppressed exposes that list. Do not confuse it with getCause: a cause explains an exception wrapping another, whereas a suppressed failure retains an additional error encountered while leaving the same operation.

Management also covers incomplete initialization of the resource set. If A opens but openB fails, work never starts and A is closed. If that close also fails, the openB failure remains primary. Management does not call close on a B variable that was never initialized. If openB internally acquired resources before failing, that operation must manage its own partial acquisition. When reviewing a snippet, separately list resources acquired, body execution, close attempts and the error delivered to the caller.

Read files under an explicit contract

Files.readString(path, StandardCharsets.UTF_8) specifies byte interpretation. A malformed sequence may produce IOException; this API does not promise to repair the file silently. Whole-file reading suits the bounded small files in these examples. For large or unbounded input, choose a design controlling memory and progressive consumption, such as a reader managed by try-with-resources, with domain-appropriate limits.

Path existence is a separate issue. Files.exists is an observation that may become stale immediately. true guarantees neither read access nor that another participant will not remove or replace the file before opening. false can also mean existence could not be determined. Do not use a preliminary check to dispense with handling the read operation itself.

Record the failure stage without guessing the cause. “It existed, but reading failed” alone proves neither deletion, permissions nor invalid bytes. Exception type, cause and operation context help distinguish these possibilities. Preserve them without exposing file contents.

A Path is not access authorization

Path represents a path within a filesystem. resolve does not force an argument to stay beneath the base: on Unix, resolving /tmp/input.txt against /srv/import returns absolute path /tmp/input.txt. normalize processes redundant components lexically. It proves neither file existence nor symbolic-link targets.

Even a result passing startsWith(base) may lead elsewhere when opened. If /srv/import/redirect points to /private, path /srv/import/redirect/item.txt has the expected prefix yet may open /private/item.txt. The question tests the difference between path shape and the object reached, without performing access.

To design an access boundary, define allowed inputs, link policy and who may alter involved directories. toRealPath consults the filesystem and resolves links by default for an existing file, but checking and then opening still requires analysis of changes possible in between. Do not present normalize or startsWith alone as a complete security boundary. Protection must match the operation and controlled environment.

Publish a version: capability and guarantee

Publication may prepare a temporary file, close writing and then attempt to make it visible as the target. The intended outcome is that readers see a complete version. Files.move with ATOMIC_MOVE requires an atomic filesystem operation; if unavailable, the API throws AtomicMoveNotSupportedException. Removing the option and retrying changes the operation’s guarantee.

Another detail matters when the target exists: with ATOMIC_MOVE, remaining options are ignored, and the general contract leaves replacement or failure to the implementation. Adding REPLACE_EXISTING does not turn that combination into a universal promise. An application supporting several providers must establish the required capability in each relevant environment.

If policy requires retaining the previous version until atomic replacement is possible, deleting the target first creates a forbidden gap. Report a missing capability instead of silently publishing differently. This case addresses change visibility with a single writer. It establishes neither power-loss durability nor permission to retry blindly after any IOException. Those requirements need separate evidence.

Guided review: load without publishing failure

The configuration service has three distinct outcomes: valid nonempty reading, valid empty reading and read failure. In the snippet, loadConfig returns a String only after complete reading; on IOException, it adds context while retaining the cause. It does not publish. Thus its caller proceeds to validation and updating only after receiving a normal result.

For an access failure, service policy keeps the active version and records a failed attempt. For a genuinely empty file, apply domain policy, which here allows disabling optional rules. Returning the previous version as fallback may maintain service operation, but must not be described as a successful new read.

When selecting an answer, trace who decides, which state may change and what evidence remains. Avoid broad catch clauses converting all failures to empty text and finally blocks returning success. Questions use single or multiple selections and snippet interpretation. No file access or program execution is needed to answer.

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

final class ConfigurationReader {
    // Contract: a small, bounded UTF-8 configuration file.
    static String loadConfig(Path path) throws IOException {
        try {
            return Files.readString(path, StandardCharsets.UTF_8);
        } catch (IOException cause) {
            throw new IOException("Configuration read failed", cause);
        }
    }
}
IN PRACTICE

A valid zero-byte read returns "". An I/O failure propagates as IOException with its original cause. The caller publishes only after successful reading and validation; this method does not change active configuration.

Common pitfalls

Returning success in finally; losing closing failures; treating exists as reservation; confusing normalize with containment; removing ATOMIC_MOVE without revisiting the contract.

Related topics: Methods, contracts and control flow · Objects, interfaces and records · Streams, lambdas and absence · Modules, delivery and diagnosis

Take this idea with you

Cleanup does not imply success. Preserve the original error, check the actual operation and promise only guarantees supported by the contract and environment.

Create account

Reference: JLS 25: Try, finally and resource management · Java SE 25; no preview features