# NOTES — seven files, three failure kinds, and one resource that has to be closed

Compile and run from the directory holding the sources:

```
..\..\..\.toolchain\jdk-21\bin\javac.exe -d out *.java
..\..\..\.toolchain\jdk-21\bin\java.exe -cp out Main
```

Plain `javac` exits 0 with no output. Real output, from exactly this code, unedited:

```
== 1. which condition wins when several apply ==
  IllegalArgumentException: malformed path (empty segment): /notes//missing.txt
  NotADirectoryException: /notes/todo.txt is not a directory
  NoSuchPathException: no such path: /notes/absent.txt

== 2. the caller branches on which domain failure it was ==
  read /notes/todo.txt -> renew passport
  read /notes/absent.txt -> offer to create it at /notes/absent.txt
  read /notes/todo.txt/x -> /notes/todo.txt is a file; stop walking
  exists("/notes/absent.txt") -> false   (the caller who cannot handle a throw asks first)

== 3. try-with-resources: both close, in reverse order ==
  | BEGIN EXPORT all
  | BEGIN EXPORT notes
  |   /notes/todo.txt -> renew passport
  |   /notes/2026/plan.txt -> ship the trainer
  | END EXPORT notes (1 entries)
  | END EXPORT all (1 entries)

== 4. the resource declared outside the parentheses ==
  session.isClosed() -> false
  | BEGIN EXPORT leaked
  |   /notes/todo.txt -> renew passport
  no END EXPORT line, so nothing may read this export

== 5. the body's exception wins; close()'s is attached ==
  caught: NoSuchPathException
  suppressed: ExportFailedException: sink refused the flush of export nightly after 1 entries
  its cause: java.io.IOException: no space left on device
  --- printStackTrace() ---
NoSuchPathException: no such path: /notes/absent.txt
	at TinyFileSystem.read(TinyFileSystem.java:71)
	at Main.suppressedException(Main.java:100)
	at Main.main(Main.java:20)
	Suppressed: ExportFailedException: sink refused the flush of export nightly after 1 entries
		at ExportSession.close(ExportSession.java:62)
		at Main.suppressedException(Main.java:98)
		... 1 more
	Caused by: java.io.IOException: no space left on device
		... 3 more
  | BEGIN EXPORT nightly
  |   /notes/todo.txt -> renew passport
  | END EXPORT nightly (1 entries)

== 6. a return inside finally discards the exception ==
  swallow() returned: looked fine to me
  the NoSuchPathException it threw is gone; nothing recorded it

== 7. finally runs even after return, and cannot alter it ==
  body: the return expression is evaluated here
  finally: runs now, after that, before the caller is resumed
  returned: the body's value
  entriesWritten() returned: 0   (finally set the local to 99 after the value was taken)
```

Four lines in there are the lesson. `session.isClosed() -> false`, the `Suppressed:` line,
`swallow() returned: looked fine to me`, and `entriesWritten() returned: 0`.

One more result worth having, because it says where the compiler can and cannot help:

```
> javac -Xlint:all -d out *.java
Main.java:128: warning: [finally] finally clause cannot complete normally
        }
        ^
```

That is section 6, found. Section 4 is the resource that never closes, and it draws no warning
from `-Xlint:all` and no error from anything. The trap the compiler catches is the smaller of the
two.

---

## `FileSystemException.java`

```java
public abstract class FileSystemException extends RuntimeException {
```

**`abstract` means nobody can throw the root itself.** So every throw site has to pick which of
the four conditions it is reporting, and a `catch (FileSystemException e)` is always catching a
choice somebody made rather than a shrug. Drop `abstract` and `throw new FileSystemException("something went wrong")`
becomes available, which is where an exception hierarchy starts collapsing back into one type.

**`extends RuntimeException`, so every domain failure here is unchecked.** That is a decision, not
a default, and it is the one to be able to defend. A caller can rule out all four
conditions first with `exists()` or `kindOf()`. The contract guarantees one caller at a time, so no
window opens between the check and the call. Make this `extends Exception` instead and all eight
`FileSystemApi` methods grow a `throws` clause. Nothing is safer, and every caller is noisier.

**The root exists so a caller can catch one thing.** `TreeResolver.find` in the reference does
exactly that: one `catch (FileSystemException notThere)` turns any of the four into `null`. Without
the root that is a four-way multi-catch that has to be edited whenever the hierarchy grows, and
the shortcuts curveball does grow it, with `SymlinkLoopException`.

## `NoSuchPathException.java`

```java
    public NoSuchPathException(String path) {
        super("no such path: " + path);
        this.path = path;
    }
```

**The constructor builds the message, so no throw site can forget it.** Every one of these is
`throw new NoSuchPathException(path)`, and the message is right by construction. The alternative,
`throw new RuntimeException("not found")` at each site, is where messages drift and stop naming the
value.

**`this.path` is kept as a field, not only interpolated into the message.** That is what makes
`missing.path()` usable in section 2's `catch`, so the caller offers to create that exact path.
Without the field a caller wanting the path has to parse `getMessage()`, and a hierarchy that
forces callers to parse strings has failed at the thing it was for.

## `PathSyntax.java`

```java
        if (!path.startsWith("/")) {
            throw new IllegalArgumentException("path must be absolute (start with '/'): " + path);
        }
```

**`IllegalArgumentException`, not a `FileSystemException`.** This is the caller-bug half of the
lesson. The test is one question you can ask about any failure: could any state of the system have
made this call succeed? For `"notes"` the answer is no, whatever the tree holds. So there is
nothing for a caller to catch and no recovery to write, and the exception belongs outside the
hierarchy a caller branches on.

**The message carries the offending path.** `path must be absolute (start with '/'): notes` needs
no debugger. `IllegalArgumentException("bad path")` sends you to the call site to find out which
one.

**This class never looks at the tree.** That is what keeps the distinction free rather than a
discipline. Fold these checks into the walk and every early return is answering two questions at
once, and the exception it throws stops following from the question it answered.

## `TinyFileSystem.java`

```java
    public String read(String path) {
        List<String> segments = PathSyntax.segments(path);
        requireWalkable(segments);
        String content = files.get(PathSyntax.join(segments));
        if (content == null) {
            throw new NoSuchPathException(path);
        }
        return content;
    }
```

**Three statements, three conditions, and the order is the contract.** `read("/notes//missing.txt")`
is malformed *and* missing. Line one wins, so the tree is never consulted, and section 1 prints
`IllegalArgumentException` rather than `NoSuchPathException`. Move `requireWalkable` above
`segments` and a malformed path starts being reported as a domain outcome, which is a contract
change the tests will catch.

**`read("/notes/todo.txt/deeper.txt")` is blocked *and* missing.** `requireWalkable` runs first, so
the caller is told `/notes/todo.txt is not a directory` and gets the blocking segment rather than
the whole path. That is the more useful of the two answers, which is why it is checked first.

**`content == null` is the only place "missing" is decided.** One condition, one throw, one message.
Returning `null` here instead would push a null check into every caller and lose the path.

```java
    public boolean exists(String path) {
        try {
            read(path);
            return true;
        } catch (FileSystemException notThere) {
```

**Catching the root, not `Exception`.** A malformed path throws `IllegalArgumentException`, which is
not a `FileSystemException`, so it travels straight out of `exists`. That is deliberate: asking
whether `"/notes//x"` exists is still a caller bug. Widen this to `catch (Exception e)` and
`exists("/notes//x")` starts answering `false`, which hides the bug and reports a lie.

**`exists` is why every failure in this hierarchy could stay unchecked.** A caller that cannot cope
with a throw has a cheap way to avoid one. Take `exists` out of the contract and the argument for
unchecked gets much weaker, because there is no longer a check to have made first.

**The variable is named `notThere`, not `e`.** The name is the only place the reason for an
otherwise-empty branch can live. A reviewer reading `catch (FileSystemException notThere)` knows
this is a decision.

## `ExportSession.java`

```java
public final class ExportSession implements AutoCloseable {
```

**`implements AutoCloseable` is the whole design statement.** Java has no destructor to write, so
this interface is the only way the class can say "somebody owes me a `close()`". Two things follow
mechanically. The compiler permits `try (ExportSession s = ...)`, and it refuses it otherwise:
`error: incompatible types: try-with-resources not applicable to variable type`. Take the interface
off and the class still compiles, still works, and now nothing in its declaration warns a caller.

```java
    public static ExportSession open(String name, StringBuilder sink, boolean sinkRefusesFlush) {
        sink.append("BEGIN EXPORT ").append(name).append('\n');
        return new ExportSession(name, sink, sinkRefusesFlush);
    }
```

**The header is written during `open`, which is what makes an unclosed session detectable.** The
export is half-written, and section 4 shows the consequence: a `BEGIN EXPORT` with no `END EXPORT`.
An `open` that wrote nothing would leave a missing `close()` invisible, and the bug would surface
somewhere else entirely.

```java
    @Override
    public void close() throws ExportFailedException {
        if (closed) {
            return;
        }
        closed = true;
```

**`if (closed) return;` makes `close()` idempotent.** `try`-with-resources calls it once, but a
caller that also calls it by hand, or a nested session, must not write a second footer.
`AutoCloseable`'s javadoc asks for this and does not enforce it, so it is on you.

**`closed = true` comes before anything that can throw.** A failed close is still a close. Without
this line, the throw below leaves `closed` false, a retry writes a second footer, and
`session.entry(...)` is still accepted on a session whose footer is already out.

```java
        if (sinkRefusesFlush) {
            throw new ExportFailedException(
                    "sink refused the flush of export " + name + " after " + entries + " entries",
                    new java.io.IOException("no space left on device"));
        }
```

**`throws ExportFailedException` on `close()` is legal because `AutoCloseable.close()` declares
`throws Exception`.** That is the difference from `java.io.Closeable`, whose `close()` narrows to
`throws IOException`. If you implement `Closeable` you cannot throw your own checked type from
`close()`.

**Checked, and this is the case that earns it.** No `canFlush()` exists to ask first, and the sink
is outside the process. Make it unchecked and a caller can ignore a failed export with no
acknowledgement anywhere. Section 5's `catch` clause exists because `javac` demanded it:
`error: unreported exception ExportFailedException; must be caught or declared to be thrown`,
reported at the `try` line with `exception thrown from implicit call to close() on resource variable 'session'`.

**The message names the export and the count; the cause is the real reason.** `getCause()` keeps
the `IOException` reachable, and section 5 prints it. Wrapping without a cause is the version of
this that loses the diagnosis.

## `Main.java`

```java
        try (ExportSession outer = ExportSession.open("all", sink, false);
             ExportSession inner = ExportSession.open("notes", sink, false)) {
```

**Two resources in one header, closing in reverse declaration order.** The output shows
`END EXPORT notes` before `END EXPORT all`, matching C++ destructor order. Two nested `try`
statements would do the same; one header is the shorter version, and the order is specified rather
than incidental.

```java
        ExportSession session = ExportSession.open("leaked", sink, false);
        try {
            session.entry("/notes/todo.txt", fs.read("/notes/todo.txt"));
```

**Section 4 exists to be wrong, and to be wrong silently.** One line moved out of the parentheses,
and `session.isClosed()` prints `false`. No error, no warning, not from `-Xlint:all` either. This
is the price of `try`-with-resources being opt-in per statement instead of automatic per scope, and
it is the single most useful thing to have seen once.

```java
        } catch (ExportFailedException | FileSystemException e) {
```

**Multi-catch, because these two need identical handling and share no useful supertype.** Their
nearest common ancestor is `Exception`, and catching that would also catch a `NullPointerException`
from the body. The `e` in a multi-catch is implicitly final, so you cannot reassign it.

```java
            for (Throwable suppressed : e.getSuppressed()) {
```

**`getSuppressed()` is where the exception `close()` threw actually lives.** The body's
`NoSuchPathException` propagated and won. Without this loop the `ExportFailedException` is present
in the object and absent from your logs, because most logging calls print only `getMessage()`.
Hand-rolling the same cleanup in a `finally` that calls `close()` is worse: `close()`'s exception
would replace the body's, and the original would be gone with nothing attached.

```java
    private static String swallow() {
        try {
            throw new NoSuchPathException("/notes/absent.txt");
        } finally {
            return "looked fine to me";
        }
    }
```

**The `return` in the `finally` block is what discards the exception.** `finally` completing by
`return` means the method completed normally, and a normal completion carries no exception. The
caller sees a plausible string and no failure was recorded anywhere. Delete the `return` and the
`NoSuchPathException` propagates as written.

**`javac -Xlint:finally` finds this one**, with `warning: [finally] finally clause cannot complete normally`
pointed at the closing brace. It is off by default and it is a warning, so nothing fails.

```java
    private static int entriesWritten() {
        int written = 0;
        try {
            return written;
        } finally {
            written = 99;
        }
    }
```

**The returned value is taken before `finally` runs, so this returns 0.** The sequence is: evaluate
the return expression, keep the value, run `finally`, hand the kept value back. Assigning to the
local afterwards has nothing to assign to. The same code returning a mutable object *would* let
`finally` change what the caller sees, and that is the version worth being careful about.
