---
title: Try-with-Resources
source: 'https://academia.sh/en/courses/java-object-model/try-with-resources'
course: 'Object-Oriented Java'
language: en
updated: '2026-08-17T18:09:41+00:00'
license: 'CC BY-SA 4.0'
---

# Try-with-Resources

When the body and the close fail at the same time, the hand-written form fails to reach one of the two events, while try-with-resources reaches both. The measurement shows, together, the count of lost and kept exceptions, closing's reverse order, and the extra five instructions the compiler generates.

In the previous lesson, every measurement had a single event: an
exception got thrown, declared, and caught. In a real body, an event is
often not alone. If a resource got opened, that resource has to be
closed while the body is failing, and the close can fail too. In that
case, two exceptions show up at once, and both have to go up the same
path.

Releasing a resource with a context manager was built in the Python
Fundamentals course; that mechanism is not repeated here. The question
here is numeric: when two events show up, how many can be reached,
which one becomes unreachable, and in what order does closing happen?
This course's question changes shape in this lesson: the party
deciding is not a type, it is **who wrote the code** — a hand-written
close and a compiler-generated close give separate results for the
same body.

## The Lost Exception and the Carried Exception

- **GE21** — The resource class gets defined within the lesson; every
  open and close gets written to a log, and the log gets cleared
  before every measurement.
- **GE22** — How many events each measurement produces is known from
  the setup and sits as the denominator in the table; reached count is
  counted from the escaped exception and the suppressed exceptions it
  carries.
- **GE23** — Only the message text gets printed from the exceptions;
  the stack trace and location do not.

```java
// Closing.java - hand-written closing next to try-with-resources
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Event extends Exception {
    Event(String m) { super(m); }
}

class Resource implements AutoCloseable {
    static final List<String> log = new ArrayList<>();
    private final String name;
    private final boolean failsOnClose;

    Resource(String name, boolean failsOnClose) {
        this.name = name;
        this.failsOnClose = failsOnClose;
        log.add("opened " + name);
    }

    @Override public void close() throws Event {
        log.add("closed " + name);
        if (failsOnClose) throw new Event("close " + name);
    }
}

interface Trial {
    void run() throws Event;
}

public class Closing {
    static void manual() throws Event {
        Resource r = new Resource("A", true);
        try {
            throw new Event("body");
        } finally {
            r.close();
        }
    }

    static void tryWithResources() throws Event {
        try (Resource r = new Resource("A", true)) {
            throw new Event("body");
        }
    }

    static void closeOnly() throws Event {
        try (Resource r = new Resource("A", true)) {
            Resource.log.add("body intact");
        }
    }

    static void twoClosing() throws Event {
        try (Resource a = new Resource("A", true); Resource b = new Resource("B", true)) {
            Resource.log.add("body intact");
        }
    }

    static void threeResources() throws Event {
        try (Resource a = new Resource("A", false);
             Resource b = new Resource("B", false);
             Resource c = new Resource("C", false)) {
            Resource.log.add("body intact");
        }
    }

    static void report(String label, int events, Trial t) {
        Resource.log.clear();
        String escaped = "-";
        int reached = 0;
        try {
            t.run();
        } catch (Event e) {
            escaped = e.getMessage();
            reached = 1 + e.getSuppressed().length;
            List<String> suppressed =
                    Arrays.stream(e.getSuppressed()).map(Throwable::getMessage).toList();
            if (!suppressed.isEmpty()) escaped += " + " + suppressed;
        }
        System.out.printf("%-16s %-26s %-9s %s%n", label, escaped, reached + " / " + events,
                Resource.log);
    }

    public static void main(String[] args) {
        System.out.printf("%-16s %-26s %-9s %s%n", "measurement", "escaped (+ suppressed)",
                "reached", "log");
        report("manual", 2, Closing::manual);
        report("try-with-resources", 2, Closing::tryWithResources);
        report("close only", 1, Closing::closeOnly);
        report("two closes", 2, Closing::twoClosing);
        report("three resources", 0, Closing::threeResources);
    }
}
```

```
measurement      escaped (+ suppressed)     reached   log
manual           close A                    1 / 2     [opened A, closed A]
try-with-resources body + [close A]           2 / 2     [opened A, closed A]
close only       close A                    1 / 1     [opened A, body intact, closed A]
two closes       close B + [close A]        2 / 2     [opened A, opened B, body intact, closed B, closed A]
three resources  -                          0 / 0     [opened A, opened B, opened C, body intact, closed C, closed B, closed A]
```

The first two rows are this lesson's measurement. Same body, same
resource, same two events: one exception in the body, one in the
close. In the hand-written form, the only exception reaching the
caller is the **close** exception; the body's exception stops nowhere
and gets **lost**. Reached count is one of two. In try-with-resources,
reached count is two of two: the escaped exception is the **body's**,
and the close exception gets carried as its **suppressed** exception.

The reason for the loss is in the **meaning of `finally`**. If an
exception comes out of `finally`, it **replaces** the exception already
on its way; two exceptions cannot travel up at once, and the
hand-written form of the language has no place to keep the second one.
Try-with-resources opens exactly that place: the body's exception stays
on the way, and the close's exception gets attached to it.

The measured cost of the loss is **which event the defect shows up
as**. In the hand-written form, the caller sees the message "close A";
the actual event that stopped the program, though, is in the body, and
that event leaves no trace anywhere. A resource that fails while
closing **covers** everything that failed before it.

## Closing Order

The third, fourth, and fifth rows' logs write out closing's order. In
the three-resource measurement, opening order is `A, B, C`, closing
order is `C, B, A`. Resources get closed in the **reverse** order of
opening, and this is not arbitrary: a resource opened later may depend
on one opened earlier, never the other way around.

The fourth row shows the same rule together with two events. `A` and
`B` get opened, the body finishes intact, both fail while closing. The
exception reaching the caller is `B`'s close exception — that is, the
one closed **first**, the one opened **later**. `A`'s gets written as
its suppressed exception. Order decides which one comes out here too:
whichever exception is first on the way, the rest get attached to it.

In the fifth row, no resource fails while closing, and reached event
count is zero; the log still shows three opens and three closes.
Closing happens in every case, whether there is an exception or not.

## The Declared Type Decides Eligibility as a Resource

- **GE24** — The three parts of the Scale core used in this lesson get
  written: compiling, a method's instruction count, and the names of
  the methods it calls.
- **GE25** — Across the six attempts, the types and bodies are fixed;
  only the type the resource gets declared with, the line in the
  body, and the enclosing method's declaration change.

```java
// Scale.java - shared measurement core: compiles a class file and reads it
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.InvokeInstruction;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Scale {
    static ClassModel compile(String source, String className) throws Exception {
        Path dir = Files.createTempDirectory("scale");
        Path file = dir.resolve(className + ".java");
        Files.writeString(file, source);
        PrintWriter silent = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow()
                .run(silent, silent, "-d", dir.toString(), file.toString()) != 0)
            throw new IllegalStateException("did not compile: " + className);
        return ClassFile.of().parse(dir.resolve(className + ".class"));
    }

    static int instructionCount(String source, String className, String methodName) throws Exception {
        int count = 0;
        for (MethodModel m : compile(source, className).methods())
            if (m.methodName().stringValue().equals(methodName))
                for (CodeElement e : m.code().orElseThrow())
                    if (e instanceof Instruction) count++;
        return count;
    }

    static List<String> calls(String source, String className, String methodName) throws Exception {
        List<String> names = new ArrayList<>();
        for (MethodModel m : compile(source, className).methods())
            if (m.methodName().stringValue().equals(methodName))
                for (CodeElement e : m.code().orElseThrow())
                    if (e instanceof InvokeInstruction iv)
                        names.add(shortName(iv.owner().asInternalName()) + "." + iv.name().stringValue());
        return names;
    }

    static String shortName(String internalName) { return internalName.substring(internalName.lastIndexOf('/') + 1); }
}
```

```java
// Eligible.java - when can a name be a resource
public class Eligible {
    static final String TYPES = """
        class Event extends Exception { Event(String m) { super(m); } }
        class Closing implements AutoCloseable { @Override public void close() throws Event { } }
        class Silent implements AutoCloseable { @Override public void close() { } }
        class NotClosing { public void close() { } }
        """;

    static final String DECLARING = TYPES + "class C { static void y() throws Event { %s } }";
    static final String NOT_DECLARING = TYPES + "class C { static void y() { %s } }";

    static void attempt(String pattern, String label, String body) {
        try {
            Scale.compile(pattern.formatted(body), "C");
            System.out.printf("%-42s -> compiled%n", label);
        } catch (Exception e) {
            System.out.printf("%-42s -> did not compile%n", label);
        }
    }

    public static void main(String[] args) {
        System.out.println("enclosing method declares Event");
        attempt(DECLARING, "try (Closing k = new Closing())", "try (Closing k = new Closing()) { }");
        attempt(DECLARING, "try (Object k = new Closing())", "try (Object k = new Closing()) { }");
        attempt(DECLARING, "try (NotClosing k = new NotClosing())",
                "try (NotClosing k = new NotClosing()) { }");
        attempt(DECLARING, "reassigned inside body k = new Closing()",
                "try (Closing k = new Closing()) { k = new Closing(); }");
        System.out.println();
        System.out.println("enclosing method declares nothing");
        attempt(NOT_DECLARING, "try (Closing k = new Closing())", "try (Closing k = new Closing()) { }");
        attempt(NOT_DECLARING, "try (Silent k = new Silent())", "try (Silent k = new Silent()) { }");
    }
}
```

```
enclosing method declares Event
try (Closing k = new Closing())            -> compiled
try (Object k = new Closing())             -> did not compile
try (NotClosing k = new NotClosing())      -> did not compile
reassigned inside body k = new Closing()   -> did not compile

enclosing method declares nothing
try (Closing k = new Closing())            -> did not compile
try (Silent k = new Silent())              -> compiled
```

The second row ties this lesson to the course's axis. The same object
— runtime type `Closing`, that is, a closable object — **cannot be a
resource** once named as `Object`. Even though the object is closable,
the compiler does not generate the close call, because it looks at the
**declared type** to decide whether a call to generate exists at all.
Try-with-resources's entire mechanism gets built at compile time, and
the object's actual class never gets asked.

The third row is the same rule's lower bound: a class that has a
method named `close` but does not take on the closability
**contract** cannot be a resource. The name is not enough; what is
needed is the declared type carrying that contract.

The fourth row shows the resource variable cannot get rebound inside
the body. The reasoning for this is the generated code itself: the
compiler generates the close call for the object bound at the
**start**, and if the variable could be rebound, the object closed and
the object opened would come apart.

The last two rows are the previous lesson's narrowing rule's
counterpart here. When the `close` method declares a checked
exception, the try-with-resources block carries that exception to the
caller, and the enclosing method has to make a choice. The same block
produces no requirement when `close` declares nothing. The cost
try-with-resources writes to the caller matches the resource's close
declaration **exactly**.

## The Code the Compiler Generates

- **GE26** — The measured source gets written within the lesson; both
  methods' bodies do the same work, only the way the close gets
  written changes.

```java
// Generated.java - what try-with-resources produces in the class file
public class Generated {
    static final String SOURCE = """
        class Event extends Exception { Event(String m) { super(m); } }
        class Resource implements AutoCloseable {
            void work() { }
            @Override public void close() throws Event { }
        }
        class C {
            static void manual() throws Event {
                Resource r = new Resource();
                try { r.work(); } finally { r.close(); }
            }
            static void tryWithResources() throws Event {
                try (Resource r = new Resource()) { r.work(); }
            }
        }
        """;

    public static void main(String[] args) throws Exception {
        for (String y : new String[] {"manual", "tryWithResources"})
            System.out.printf("%-18s instructions %2d  calls %s%n", y,
                    Scale.instructionCount(SOURCE, "C", y), Scale.calls(SOURCE, "C", y));
    }
}
```

```
manual             instructions 15  calls [Resource.<init>, Resource.work, Resource.close, Resource.close]
tryWithResources   instructions 20  calls [Resource.<init>, Resource.work, Resource.close, Resource.close, Throwable.addSuppressed]
```

The form that is two lines shorter in the source is **five
instructions longer** in the class file: the hand-written version is
15, the try-with-resources version 20. Try-with-resources is a
**shortening**, but what it shortens is the written text; the code it
generates does extra work.

Two `close` calls show up in the call lists: one for the path that
runs when the body finishes cleanly, one for the path that runs when
the body fails. Both of these exist in the hand-written form too,
because the compiler places the `finally` block on both paths. What
makes the difference is the **fifth** call, showing up only in
try-with-resources: the **suppression** call. The extra five
instructions belong to this too — holding the exception escaping the
body, trying the close on a separate branch, and attaching the second
exception to the first.

What the measurement says: a suppressed exception is not something
that happens on its own at the language's runtime. There is a call,
inside the code closing the resource, that asks for suppression, and
the compiler put that call there; someone writing the same call by
hand would get the same result. What try-with-resources earns is not
a new capability, it is that code getting written **every time, and
in the same way**.

## A Closing Exception Does Not Suppress the Body's

This lesson's boundary measurement sits in comparing the second and
third rows, and shows that suppression's direction runs one way.

In the second row, both the body and the close fail, and the escaped
exception is the **body's**; the close's gets carried as suppressed.
The direction never reverses: an exception failing during close cannot
suppress the body's, because the body's exception is already on the
way and gives its place to no one.

The third row draws this rule's boundary. When the body finishes
intact, there is nothing to suppress; the close exception comes out
**directly**, and its suppressed list is empty. Suppression, that is,
is not a hiding mechanism, it is only a place carrying a **second**
event. A defect in closing loses nothing of its visibility when it is
alone.

The rule these two rows say together: try-with-resources destroys no
event and changes no event's priority. The exception lost in the
hand-written form was lost not because the priority order was wrong,
but because there was **no place to put a second event**.

## Summary

- When the body and the close fail at the same time, only one of two
  events gets reached in the hand-written form; both get reached with
  try-with-resources.
- In the hand-written form, an exception coming out of `finally`
  replaces the one on its way, and no trace of the body's event
  remains; the caller sees the close event, not the event that
  stopped the program.
- Resources get closed in the reverse order of opening; when more
  than one close fails, the first one closed's exception comes out,
  and the rest get written to it as suppressed.
- The declared type decides whether a name can be a resource: a
  closable object does not get closed once named as `Object`, and the
  block writes to the caller only as much cost as the close
  declaration carries.
- Try-with-resources is five instructions longer than the hand-written
  form in the class file; what shortens is the written text, not the
  generated code.
- Boundary measurement: suppression's direction runs one way. A
  closing exception does not suppress the body's, only the reverse
  happens; when the body finishes intact, the closing exception comes
  out directly and alone.

## Next Step

Across three lessons, an exception was always a given: it got thrown,
declared, caught, suppressed. None of them asked whether an event
**should be reported as an exception at all**. Yet the same work can
be written with three separate contracts: a checked exception, an
unchecked exception, or no exception at all, returning the result
through a type. This course's last lesson builds all three around the
same work, counts how many decision points the compiler forces on the
calling side, and measures where a defect shows up once a contract
breaks.
