Try-with-resources and return statements in java

Based on Oracle's tutorial, "[the resource] will be closed regardless of whether the try statement completes normally or abruptly". It defines abruptly as from an exception.

Returning inside the try is an example of abrupt completion, as defined by JLS 14.1.


The resource will be closed automatically (even with a return statement) since it implements the AutoCloseable interface. Here is an example which outputs "closed successfully":

public class Main {

    public static void main(String[] args) {
        try (Foobar foobar = new Foobar()) {
            return;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Foobar implements AutoCloseable {

    @Override
    public void close() throws Exception {
        System.out.println("closed successfully");
    }
}

The AutoCloseable interface can make the execution order of code confusing at first glance. Lets run through this with an example:

public class Main {

    // An expensive resource which requires opening / closing
    private static class Resource implements AutoCloseable {

        public Resource() {
            System.out.println("open");
        }
        
        @Override public void close() throws Exception {
            System.out.println("close");
        }
    }
    
    // find me a number!
    private static int findNumber() {
        // open the resource
        try(Resource resource = new Resource()) {
            // do some business logic (usually involving the resource) and return answer
            return 2 + 2;
        } catch(Exception e) {
            // resource encountered a problem
            throw new IllegalStateException(e);
        }
    }
    
    public static void main(String[] args) {
        System.out.println(findNumber());
    }
}

The above code attempts to open some Resource and conduct some business logic using the resource (just some arithmetic in this case). Running the code will print:

open
close
4

Therefore the Resource is closed before exiting the try-with-resource block. To make it clear what exactly is going on, lets reorganise the findNumber() method.

    private static int findNumber() {
        // open the resource
        int number;
        try(Resource resource = new Resource()) {
            // do some business logic and return answer
            number = 2 + 2;
        } catch(Exception e) {
            // resource encountered a problem
            throw new IllegalStateException(e);
        }
        return number;
    }

Conceptually, this is what happens under the hood when return is placed inside a try-with-resource block. The return operation is moved to after the try-with-resource block to allow the AutoCloseable object to close before returning.

Therefore we can conclude that a return operation inside a try-with-resource block is just syntactic sugar and you need not worry about returning before an AutoCloseable has closed.


Good answers have already been posted. I'm just taking a different approach as it feels like an opportunity to dive into some details that may some day be handy, which is trying to answer the question by reading some bytecode.

There are a few scenarios - to look at

  • exception in the try block
  • exception when closing the auto-closeable during exiting on the try block
  • exception when closing the auto-closeable resource during handling an earlier exception
  • return in the try block, is close executed prior to return.

The first scenario is usually top of mind with using try-with in java. We can try understanding the other three scenarios by looking at the byte code. The last scenario addresses your question.

Breaking down the byte code for the main method below

import java.io.*;

class TryWith {

  public static void main(String[] args) {
    try(PrintStream ps = System.out) {
       ps.println("Hey Hey");
       return;
    }
  }
}

Lets review it in small parts (some details elided)

    Code:
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: astore_1

0: get the static field System.out.
3: store the field into the LocalVariableTable (lvt) at slot 1.

Reviewing the lvt we can confirm that the first slot is of the java.io.PrintStream and it has the name ps

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4      35     1    ps   Ljava/io/PrintStream;
            0      39     0  args   [Ljava/lang/String;
         4: aload_1
         5: ldc           #3                  // String Hey Hey
         7: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

4: Load ps (aload_1)
5: Load the constant (ldc), hey hey from the constant pool.
7: Invoke the print line method, this consumes ps and hey hey from the operand stack.

        10: aload_1
        11: ifnull        18
        14: aload_1
        15: invokevirtual #5                  // Method java/io/PrintStream.close:()V
        18: return

10 - 11: load ps onto the operand stack. check if ps is null and if it is null, jump to 18 and return from the function.
14 - 18: load ps, invoke close and return.

The above is of particularly interest because it suggest that try-with block would work if the Auto-Closeable resource is null and not throw an exception. Of course even if it did work, it would be moot - unless the resource wasn't accessed in the try block. Any access would result in a NPE.

The above is also the normal flow, what happens in the even of an exception? Lets take a look at the exception table

      Exception table:
         from    to  target type
             4    10    19   Class java/lang/Throwable
            24    28    31   Class java/lang/Throwable

This tells us that any exception of type java.lang.Throwable between byte code 4-10 is handled at target 19. Similarly for lines 24-28 at line 31.

        19: astore_2
        20: aload_1
        21: ifnull        37
        24: aload_1
        25: invokevirtual #5                  // Method java/io/PrintStream.close:()V
        28: goto          37

19: Store the exception into local variable 2.
20 - 25: This is the same pattern we saw earlier close is only invoked if ps is not null 28: a jump instruction to 37

        37: aload_2
        38: athrow

37: load the object stored in the local variable table at position 2, earlier we stored the exception in this position.
38: throw the exception

However what about the case of an exception occurring during close when the close was executing on account of an earlier exception. Lets recap the exception table

      Exception table:
         from    to  target type
             4    10    19   Class java/lang/Throwable
            24    28    31   Class java/lang/Throwable

That is the second line the exception table, lets look at the corresponding byte code at target 31

        31: astore_3
        32: aload_2
        33: aload_3
        34: invokevirtual #7                  // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
        37: aload_2
        38: athrow

31: The secondary exception is stored in the local variable at slot 3.
32: Reload the original exception from slot 3.
33-34: add the secondary exception as the suppressed exception to the original exception.
37-38: throw the new exception, we covered these lines earlier.

Revisiting our consideration listed at the beginning

  • exception when closing the auto-closeable during exiting on the try block.
    ** an exception is raised and the try block exits abruptly
  • exception when closing the auto-closeable resource during handling an earlier exception.
    ** A suppressed exception is added to the orignal exception and the original exception is thrown. the try block exits abruptly
  • return in the try block, is close executed prior to return.
    ** close is executed prior to the return in the try block

Revisiting the interesting scenarios of auto-closeable resource being null that we encountered in the byte code, we can test that with

import java.io.*;

class TryWithAnother {

  public static void main(String[] args) {
    try(PrintStream ps = null) {
       System.out.println("Hey Hey");
       return;
    }
  }
}

Not surprisingly we get the output Hey Hey on the console and no exception.

Last but pretty important to keep in mind is that this bytecode is a compliant implementation of the JLS. This approach is pretty handy to determine what your actual execution entails, there might be other compliant alternatives - in this situation I can't think of any. However with this in mind this response won't be complete without specifying my javac version

openjdk 11.0.9.1 2020-11-04
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.9.1+1)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.9.1+1, mixed mode)