How finally can silently change your answer

Core Java · lesson 38 of 42 · 6 min read

A return in finally discards the real result - including an exception.

Open this lesson in the learning hub

Key points

  • The return value is computed before finally runs. A finally block that mutates a local variable therefore does not change what is returned, because the value was already taken.
  • But a return inside finally replaces the pending result entirely - and if an exception was propagating, it is discarded without trace.
  • That is one of the few ways to lose an exception completely: no stack trace, no log, no evidence it ever happened.
  • A break or continue inside finally does the same thing, which is why most style checkers forbid all three outright.
  • The mutation rule differs for objects: reassigning the variable in finally has no effect, but mutating the object it points to does, because the reference was captured, not the contents.
  • Try-with-resources avoids the whole area. It closes resources without a finally block, and it records the close failure as a suppressed exception rather than replacing the original.

Example

import java.util.ArrayList;
import java.util.List;

public class FinallySemantics {

    // The value is captured BEFORE finally runs, so this returns 1.
    static int mutateLocal() {
        int x = 1;
        try {
            return x;          // the VALUE 1 is taken here
        } finally {
            x = 2;             // too late - the return value is already 1
        }
    }

    // A return in finally REPLACES the pending result.
    static int returnInFinally() {
        try {
            return 1;
        } finally {
            return 2;          // wins - 1 is discarded
        }
    }

    // And it discards a propagating exception entirely.
    static int swallowsException() {
        try {
            throw new IllegalStateException("this is lost forever");
        } finally {
            return -1;         // the exception vanishes; no trace at all
        }
    }

    // Reassignment does nothing, but MUTATION is visible.
    static List<String> mutateObject() {
        List<String> items = new ArrayList<>();
        try {
            items.add("a");
            return items;      // the REFERENCE is captured
        } finally {
            items.add("b");    // same object -> the caller sees this
        }
    }

    public static void main(String[] args) {
        System.out.println("mutateLocal()      = " + mutateLocal());
        System.out.println("returnInFinally()  = " + returnInFinally());
        System.out.println("swallowsException()= " + swallowsException());
        System.out.println("mutateObject()     = " + mutateObject());

        // try-with-resources RECORDS the close failure instead of losing it.
        try {
            try (AutoCloseable bad = () -> { throw new IllegalStateException("close failed"); }) {
                throw new RuntimeException("body failed");
            }
        } catch (Exception e) {
            System.out.println("primary    = " + e.getMessage());
            for (Throwable s : e.getSuppressed()) {
                System.out.println("suppressed = " + s.getMessage());
            }
        }
    }
}

Never return from finally - it discards the real result, and a propagating exception disappears without trace.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Core Java course, and every lesson in it is listed on the Core Java contents page.