Free Handbook · Every example compiled & verified

Exceptions

Java exceptions explained: checked vs unchecked, try/catch/finally, try-with-resources, custom exceptions, cause chains and how to read a stack trace.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 07 · what you'll be able to do

  • Place any exception in the Throwable / Error / Exception / RuntimeException hierarchy and say whether the compiler forces you to handle it
  • Write try/catch/finally and multi-catch blocks, and predict exactly which lines run when something throws
  • Close files, connections and your own resources reliably with try-with-resources and AutoCloseable
  • Throw and define custom exceptions, wrap low-level failures with a cause, and read the resulting "Caused by" stack trace
  • Decide when to catch, when to let an exception propagate, and spot the anti-patterns that hide bugs
01

What an exception is, and the hierarchy

An exception is an object that describes something going wrong while the program runs: dividing by zero, reading past the end of an array, a file that is not there. When one is thrown, Java stops the current method immediately and walks back up the chain of callers looking for a matching catch. If it reaches the top of main without finding one, the program dies and prints a stack trace.

javaMain.java
public class Main {
    public static void main(String[] args) {
        int[] scores = { 90, 75, 60 };
        try {
            System.out.println("before");
            System.out.println(scores[5]);     // throws here
            System.out.println("never printed");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("caught: " + e.getMessage());
        }
        System.out.println("program continues");
    }
}
Outputcompiled & run with real Java
before
caught: Index 5 out of bounds for length 3
program continues
Your turn

Remove the try/catch and run it again: "program continues" is never printed, and you get a stack trace instead.

The hierarchy

Every exception is a subclass of Throwable. Where a class sits in this tree decides how the compiler treats it, so learning the four top classes is worth more than memorising dozens of exception names.

Checked = Exception and its subclasses, except the RuntimeException branch. Unchecked = RuntimeException, Error and their subclasses.
ClassMeansExamplesCompiler forces you to handle it?
ThrowableThe root. Only Throwables can be thrown or caught——
ErrorThe JVM itself is in trouble. Do not catchOutOfMemoryError, StackOverflowErrorNo
ExceptionA problem a program can reasonably recover fromIOException, SQLException, InterruptedExceptionYes (checked)
RuntimeException (a subclass of Exception)Usually a bug in the calling codeNullPointerException, IllegalArgumentException, ArithmeticException, IndexOutOfBoundsExceptionNo (unchecked)
javaMain.java
public class Main {
    public static void main(String[] args) {
        Throwable[] samples = {
            new ArithmeticException(),
            new java.io.IOException(),
            new StackOverflowError(),
        };
        for (Throwable t : samples) {
            String kind = (t instanceof Error) ? "Error (do not catch)"
                        : (t instanceof RuntimeException) ? "unchecked"
                        : "checked";
            System.out.println(t.getClass().getSimpleName() + " -> " + kind);
        }
    }
}
Outputcompiled & run with real Java
ArithmeticException -> unchecked
IOException -> checked
StackOverflowError -> Error (do not catch)
02

Checked vs unchecked exceptions, throw and throws

A checked exception is part of a method's contract. If a method can throw one, it must say so with a throws clause, and every caller must either catch it or declare it too. The compiler enforces this — the idea is that failures like "file not found" or "network down" are normal enough that callers must think about them.

Error you will hit

unreported exception IOException; must be caught or declared to be thrown

java
import java.nio.file.*;

public class Main {
    public static void main(String[] args) {
        String text = Files.readString(Path.of("notes.txt"));
        System.out.println(text);
    }
}
Main.java:5: error: unreported exception IOException; must be caught or declared to be thrown
        String text = Files.readString(Path.of("notes.txt"));
                                      ^
1 error
Why the compiler said that

Files.readString is declared throws IOException, a checked exception. main neither catches it nor declares it, so the compiler refuses. This is a compile error — the program never ran.

The fix

Either handle it where you can do something useful (try { … } catch (IOException e) { … }), or pass it up by adding throws IOException to the method signature.

java
import java.io.IOException;
import java.nio.file.*;

public class Main {
    public static void main(String[] args) {
        try {
            String text = Files.readString(Path.of("notes.txt"));
            System.out.println(text);
        } catch (IOException e) {
            System.out.println("could not read notes: " + e.getClass().getSimpleName());
        }
    }
}

Unchecked exceptions (RuntimeException and below) need no declaration. They signal programming mistakes — a null where there should not be one, a bad argument — and the right response is usually to fix the code, not to catch them. You raise one yourself with throw:

javaMain.java
public class Main {
    static int parseAge(String text) {
        int age = Integer.parseInt(text);          // may throw NumberFormatException
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("age out of range: " + age);
        }
        return age;
    }

    public static void main(String[] args) {
        String[] inputs = { "34", "-2", "abc" };
        for (String in : inputs) {
            try {
                System.out.println("ok " + parseAge(in));
            } catch (NumberFormatException e) {
                System.out.println("not a number: " + in);
            } catch (IllegalArgumentException e) {
                System.out.println("rejected: " + e.getMessage());
            }
        }
    }
}
Outputcompiled & run with real Java
ok 34
rejected: age out of range: -2
not a number: abc

NumberFormatException is itself a subclass of IllegalArgumentException, which is why its catch must come first — see the next lesson.

<code>throw</code>

  • A statement inside a method body
  • Actually raises one exception object now
  • throw new IllegalStateException("closed");

<code>throws</code>

  • Part of a method signature
  • Declares which checked exceptions may escape
  • void load() throws IOException
In real jobs
Modern Java code leans towards unchecked exceptions. Frameworks such as Spring wrap checked SQLExceptions in unchecked ones, and lambdas in streams cannot throw checked exceptions without wrapping (see Module 09). Checked exceptions still appear all over the JDK — I/O, reflection, threads — so you must be fluent with both.
03

try, catch, finally and multi-catch

A try block can have several catch blocks, tried top to bottom; the first whose type matches the exception (including via a superclass) wins, and no others run. A finally block runs afterwards whatever happened — normal completion, a caught exception, an uncaught one, even a return from inside the try. It is where clean-up goes.

javaMain.java
public class Main {
    static String divide(int a, int b) {
        try {
            return "result " + (a / b);
        } catch (ArithmeticException e) {
            return "cannot divide by zero";
        } finally {
            System.out.println("  finally ran for " + a + "/" + b);
        }
    }

    public static void main(String[] args) {
        System.out.println(divide(10, 2));
        System.out.println(divide(1, 0));
    }
}
Outputcompiled & run with real Java
  finally ran for 10/2
result 5
  finally ran for 1/0
cannot divide by zero

Notice "finally ran" prints before the returned value: the return value is computed, then finally runs, then the method actually returns.

Visualizedivide(1, 0): where execution goesStep 1 / 6
static String divide(int a, int b) {
try {
return "result " + (a / b);
} catch (ArithmeticException e) {
return "cannot divide by zero";
} finally {
System.out.println(" finally ran for " + a + "/" + b);
}
}
Line 1

Called with a = 1, b = 0.

Variables now
a1
b0
All 6 steps as a table
StepLineWhat happenedVariables now
11Called with a = 1, b = 0.a = 1 b = 0
231 / 0 on ints throws ArithmeticException. The return never completes; the rest of the try is abandoned.
34Java checks the catch clauses in order. ArithmeticException matches.e = ArithmeticException("/ by zero")
45The catch computes its return value and prepares to leave the method.returning = "cannot divide by zero"
57Before leaving, finally always runs.
69Now the method returns the value prepared on line 5.

Multi-catch and catch order

When two exceptions need the same handling, catch them together with |: catch (NumberFormatException | ArithmeticException e). The types must not be parent and child. Separate catch blocks must go from most specific to most general, because a parent catch placed first would swallow the child's exceptions and make the later block dead code — which the compiler rejects.

javaMain.java
public class Main {
    static int compute(String a, String b) {
        return Integer.parseInt(a) / Integer.parseInt(b);
    }

    public static void main(String[] args) {
        String[][] cases = { { "8", "2" }, { "8", "zero" }, { "8", "0" } };
        for (String[] c : cases) {
            try {
                System.out.println(compute(c[0], c[1]));
            } catch (NumberFormatException | ArithmeticException e) {
                System.out.println("bad input (" + e.getClass().getSimpleName() + ")");
            }
        }
    }
}
Outputcompiled & run with real Java
4
bad input (NumberFormatException)
bad input (ArithmeticException)
Error you will hit

exception NumberFormatException has already been caught

java
public class Main {
    public static void main(String[] args) {
        try {
            int n = Integer.parseInt("12x");
            System.out.println(n);
        } catch (IllegalArgumentException e) {
            System.out.println("illegal argument");
        } catch (NumberFormatException e) {
            System.out.println("not a number");
        }
    }
}
Main.java:8: error: exception NumberFormatException has already been caught
        } catch (NumberFormatException e) {
          ^
1 error
Why the compiler said that

NumberFormatException extends IllegalArgumentException, so the first catch already handles every NumberFormatException. The second block could never run, and Java treats unreachable catch blocks as errors.

The fix

Put the more specific type first: catch (NumberFormatException e), then catch (IllegalArgumentException e).

Never return from finally
A return (or throw) inside finally replaces whatever the try was returning or throwing — an exception simply vanishes. Keep finally for clean-up only.
04

try-with-resources and AutoCloseable

Files, database connections, network sockets and locks must be closed when you are done, even if something fails halfway. Writing that by hand with finally is verbose and easy to get wrong. try-with-resources does it for you: declare the resources in parentheses after try, and Java calls close() on each one when the block ends — in reverse order of opening, whether it ended normally or by an exception.

Any class that implements AutoCloseable (one method: void close() throws Exception) can be used this way. Here is a pretend connection so you can see exactly when things happen:

javaMain.java
public class Main {
    public static void main(String[] args) {
        try (Conn db = new Conn("db"); Conn cache = new Conn("cache")) {
            db.query("SELECT 1");
            cache.query("GET user:7");
            throw new IllegalStateException("something failed mid-work");
        } catch (IllegalStateException e) {
            System.out.println("handled: " + e.getMessage());
        }
    }
}

class Conn implements AutoCloseable {
    private final String name;

    Conn(String name) {
        this.name = name;
        System.out.println("open " + name);
    }

    void query(String q) {
        System.out.println(name + " <- " + q);
    }

    @Override
    public void close() {
        System.out.println("close " + name);
    }
}
Outputcompiled & run with real Java
open db
open cache
db <- SELECT 1
cache <- GET user:7
close cache
close db
handled: something failed mid-work

Both connections are closed, newest first, before the catch block runs.

Your turn

Delete the throw line and confirm the close order is the same on the success path.

Suppressed exceptions

What if the body throws and close() throws? A hand-written finally would lose the first, more important exception. try-with-resources keeps the body's exception as the main one and attaches the close failure to it as a suppressed exception, available through getSuppressed().

javaMain.java
public class Main {
    public static void main(String[] args) {
        try (Flaky f = new Flaky()) {
            throw new RuntimeException("write failed");
        } catch (RuntimeException e) {
            System.out.println("main error: " + e.getMessage());
            for (Throwable s : e.getSuppressed()) {
                System.out.println("suppressed: " + s.getMessage());
            }
        }
    }
}

class Flaky implements AutoCloseable {
    @Override
    public void close() {
        throw new IllegalStateException("close failed too");
    }
}
Outputcompiled & run with real Java
main error: write failed
suppressed: close failed too
java
// The everyday real-world form: reading a file line by line
try (BufferedReader in = Files.newBufferedReader(Path.of("orders.csv"))) {
    String line;
    while ((line = in.readLine()) != null) {
        process(line);
    }
} // in.close() is guaranteed here, even if process() throws

Not runnable here (it needs a real file), but this is the pattern you will write most often.

Error you will hit

incompatible types: try-with-resources not applicable to variable type

java
public class Main {
    public static void main(String[] args) {
        try (Printer p = new Printer()) {
            p.print("hello");
        }
    }
}

class Printer {
    void print(String s) { System.out.println(s); }
    void close() { System.out.println("closed"); }
}
Main.java:3: error: incompatible types: try-with-resources not applicable to variable type
        try (Printer p = new Printer()) {
                     ^
    (Printer cannot be converted to AutoCloseable)
1 error
Why the compiler said that

Having a method called close() is not enough. try-with-resources works by calling AutoCloseable.close(), so the resource's type must implement that interface.

The fix

Declare it: class Printer implements AutoCloseable and mark close() as public with @Override.

java
public class Main {
    public static void main(String[] args) {
        try (Printer p = new Printer()) {
            p.print("hello");
        }
    }
}

class Printer implements AutoCloseable {
    void print(String s) { System.out.println(s); }
    @Override
    public void close() { System.out.println("closed"); }
}
05

Custom exceptions

Built-in exceptions say what broke at a low level. A custom exception says what it means for your domain — InsufficientFundsException is far clearer to a caller than a generic IllegalStateException, and it can carry useful data. Extend RuntimeException for an unchecked one, or Exception for a checked one, and pass the message (and cause, next lesson) up to the parent constructor.

javaMain.java
public class Main {
    public static void main(String[] args) {
        Wallet w = new Wallet(500);
        try {
            w.pay(200);
            w.pay(450);
        } catch (InsufficientFundsException e) {
            System.out.println(e.getMessage());
            System.out.println("top up at least " + e.shortfall());
        }
    }
}

class InsufficientFundsException extends Exception {
    private final long shortfall;

    InsufficientFundsException(long needed, long available) {
        super("need " + needed + " but only " + available + " available");
        this.shortfall = needed - available;
    }

    long shortfall() { return shortfall; }
}

class Wallet {
    private long balance;
    Wallet(long balance) { this.balance = balance; }

    void pay(long amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(amount, balance);
        }
        balance -= amount;
        System.out.println("paid " + amount + ", left " + balance);
    }
}
Outputcompiled & run with real Java
paid 200, left 300
need 450 but only 300 available
top up at least 150
Your turn

Make InsufficientFundsException extend RuntimeException instead and remove the throws clause. It still compiles — and now nothing forces callers to handle it. Which do you prefer for a payment API?

Error you will hit

unreported exception InsufficientFundsException

java
public class Main {
    public static void main(String[] args) {
        new Wallet().pay(900);
    }
}

class InsufficientFundsException extends Exception {
    InsufficientFundsException(String msg) { super(msg); }
}

class Wallet {
    void pay(long amount) {
        throw new InsufficientFundsException("need " + amount);
    }
}
Main.java:13: error: unreported exception InsufficientFundsException; must be caught or declared to be thrown
        throw new InsufficientFundsException("need " + amount);
        ^
1 error
Why the compiler said that

The class extends Exception, so it is checked. Throwing it from pay requires pay to declare throws InsufficientFundsException. After that, main will get the same error one level up until it catches or declares it.

The fix

Add throws InsufficientFundsException to pay, then handle it in the caller — or make it unchecked by extending RuntimeException if callers cannot reasonably recover.

  • Name it after the problem and end with Exception: OrderNotFoundException, RateLimitExceededException.
  • Provide the constructors (String message) and (String message, Throwable cause) at minimum.
  • Do not create one per method. A handful per module, reused, keeps catch blocks simple.
  • Prefer the standard ones when they already say it: IllegalArgumentException for bad input, IllegalStateException for "not now", UnsupportedOperationException for "never".
06

Chaining causes

A repository layer catches an SQLException; a service layer does not want to know about SQL. The right move is to wrap: throw your own higher-level exception and pass the original as its cause. The caller gets a meaningful type, and nothing is lost — getCause() returns the original, and the stack trace prints it under Caused by:.

javaMain.java
public class Main {
    public static void main(String[] args) {
        try {
            loadConfig("port=eighty");
        } catch (ConfigException e) {
            System.out.println("top:   " + e.getMessage());
            Throwable cause = e.getCause();
            System.out.println("cause: " + cause.getClass().getSimpleName() + ": " + cause.getMessage());
        }
    }

    static int loadConfig(String line) {
        String value = line.split("=")[1];
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new ConfigException("invalid port in config: " + line, e);   // keep e!
        }
    }
}

class ConfigException extends RuntimeException {
    ConfigException(String message, Throwable cause) {
        super(message, cause);
    }
}
Outputcompiled & run with real Java
top:   invalid port in config: port=eighty
cause: NumberFormatException: For input string: "eighty"
Error you will hit

Reading a chained stack trace: Caused by

java
public class Main {
    public static void main(String[] args) {
        loadConfig("port=eighty");
    }

    static int loadConfig(String line) {
        String value = line.split("=")[1];
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new ConfigException("invalid port in config: " + line, e);
        }
    }
}

class ConfigException extends RuntimeException {
    ConfigException(String message, Throwable cause) { super(message, cause); }
}
Exception in thread "main" ConfigException: invalid port in config: port=eighty
	at Main.loadConfig(Main.java:11)
	at Main.main(Main.java:3)
Caused by: java.lang.NumberFormatException: For input string: "eighty"
	at java.base/java.lang.NumberFormatException.forInputString(Unknown Source)
	at java.base/java.lang.Integer.parseInt(Unknown Source)
	at java.base/java.lang.Integer.parseInt(Unknown Source)
	at Main.loadConfig(Main.java:9)
	at Main.main(Main.java:3)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.execute(Unknown Source)
	at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.run(Unknown Source)
	at jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher.main(Unknown Source)
Why the compiler said that

The first block is the exception that escaped main: your ConfigException, thrown at line 11 inside loadConfig. The Caused by: block is the original NumberFormatException from line 9. The java.base/… and jdk.compiler/…SourceLauncher frames belong to the JDK and to the java Main.java launcher — skip them. In a packaged application you will often see ... 3 more at the end of a Caused by block instead: those frames are identical to the ones above, so Java does not repeat them. In long production traces, the last Caused by is usually the root cause.

The fix

Nothing is broken about the chaining — it is doing its job. The real fix is the data (port=80) or catching ConfigException where you can report it nicely. The bug to avoid is throw new ConfigException(msg) without e, which throws the "Caused by" section away forever.

Do not log and rethrow
Catching, logging, then rethrowing makes the same failure appear several times in the logs, once per layer. Either handle it (log and recover) or wrap and rethrow — then log once, at the top, where the full chain is visible.
07

Reading a stack trace

A stack trace looks like noise until you know its three parts. Line 1 is the exception class and its message — read it word for word, it usually tells you what went wrong. The at lines are the call stack at the moment of the throw, innermost first: the top at line is where it was thrown, each line below is the caller of the one above. Caused by blocks, if any, are wrapped originals.

  1. 1
    Read the first line

    Exception type plus message. NullPointerException: Cannot invoke "String.length()" because … is null already names the variable that was null.

  2. 2
    Find the first frame in your code

    Skip frames from java.base/ and libraries until you reach a class you wrote. That file and line number is where to start looking.

  3. 3
    Walk down to see how you got there

    The frames below show which method called which. The bug is often a caller passing bad data, not the line that threw.

  4. 4
    Jump to the last Caused by

    For wrapped exceptions, the root cause is at the bottom of the trace.

Error you will hit

NullPointerException: Cannot invoke "String.length()"

java
public class Main {
    static String nickname;                 // never assigned: null

    static int badgeWidth() {
        return nickname.length() * 8;
    }

    static void render() {
        System.out.println("width " + badgeWidth());
    }

    public static void main(String[] args) {
        render();
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "Main.nickname" is null
	at Main.badgeWidth(Main.java:5)
	at Main.render(Main.java:9)
	at Main.main(Main.java:13)
Why the compiler said that

Read it top down. Line 1: something called length() on Main.nickname, which was null. First at frame: it happened in badgeWidth at line 5. Below that: render (line 9) called it, and main (line 13) called render. These "helpful NullPointerException" messages arrived in Java 14; for local variables they show "<local1>" instead of the name unless the class was compiled with javac -g.

The fix

Make sure the field is assigned before use, or guard it: return nickname == null ? 0 : nickname.length() * 8;. Better still, initialise it: static String nickname = "";.

javaMain.java
public class Main {
    static int depth(int n) {
        if (n == 3) {
            StackTraceElement[] frames = new Throwable().getStackTrace();
            for (StackTraceElement f : frames) {
                if (!f.getClassName().equals("Main")) continue;   // skip JDK/launcher frames
                System.out.println("at " + f.getMethodName() + " line " + f.getLineNumber());
            }
            return n;
        }
        return depth(n + 1);
    }

    public static void main(String[] args) {
        depth(1);
    }
}
Outputcompiled & run with real Java
at depth line 4
at depth line 11
at depth line 11
at main line 15

You can read the stack yourself: three calls to depth sit on top of main, innermost first — exactly the shape of an exception's at lines.

08

When not to catch

Beginners tend to catch too much. An exception is information; catching it only makes sense where you can do something with it: retry, use a fallback, show the user a clear message, or translate it into a better exception. Everywhere else, let it propagate — a loud crash in development is far cheaper than silently wrong data in production.

Anti-pattern

  • catch (Exception e) { } — the bug disappears and resurfaces somewhere confusing
  • catch (Exception e) { e.printStackTrace(); } then carry on as if nothing happened
  • Catching NullPointerException or ArrayIndexOutOfBoundsException instead of fixing the check
  • Catching Throwable or Error (OutOfMemoryError cannot be "handled")
  • Using exceptions for normal control flow, like ending a loop

Better

  • Catch the narrowest type you can actually handle
  • Handle it (fallback, retry, user message) or rethrow wrapped with the cause
  • Test for the condition up front: if (i < arr.length), Objects.requireNonNull
  • Let Errors kill the process; the platform restarts it
  • Return a value that says "nothing here" — Optional, an empty list, a boolean
javaMain.java
import java.util.Optional;

public class Main {
    // Invalid input is expected here, so the method says so in its return type
    static Optional<Integer> tryParse(String s) {
        if (s == null || !s.matches("-?\\d+")) {
            return Optional.empty();
        }
        return Optional.of(Integer.parseInt(s));
    }

    public static void main(String[] args) {
        String[] fields = { "12", "", "x7", "-3" };
        int sum = 0;
        for (String f : fields) {
            Optional<Integer> n = tryParse(f);
            if (n.isPresent()) {
                sum += n.get();
            } else {
                System.out.println("skipping '" + f + "'");
            }
        }
        System.out.println("sum = " + sum);
    }
}
Outputcompiled & run with real Java
skipping ''
skipping 'x7'
sum = 9

Checking up front is clearer and much faster than throwing and catching a NumberFormatException for every bad field. Optional is covered properly in Module 09.

Fail fast at the boundary
Validate arguments at the top of public methods with Objects.requireNonNull(x, "x") or an IllegalArgumentException. A failure right at the entrance, with a clear message, saves an hour of tracing a null through five layers.
Throwable
The root of everything that can be thrown; splits into Error and Exception.
Checked exception
A subclass of Exception outside RuntimeException; must be caught or declared with throws.
Unchecked exception
RuntimeException, Error and their subclasses; no declaration required.
throw / throws
throw raises an exception now; throws in a signature declares what may escape.
finally
A block that runs after try/catch no matter how they ended; used for clean-up.
try-with-resources
try (R r = …): closes every AutoCloseable resource automatically, in reverse order.
Suppressed exception
An exception from close() attached to the main exception instead of replacing it.
Cause
The original exception wrapped inside a higher-level one; shown as Caused by:.
Stack trace
The exception, its message and the call stack at the throw point, innermost frame first.
Multi-catch
catch (A | B e): one handler for several unrelated exception types.
Quick check

A method has try { return 1; } finally { System.out.println("done"); }. What happens when it is called?

Quick check

Which of these must be caught or declared, or the code will not compile?

Frequently asked questions

What is the difference between checked and unchecked exceptions in Java?
Checked exceptions extend Exception but not RuntimeException (for example IOException); the compiler forces you to catch them or declare them with throws. Unchecked exceptions extend RuntimeException or Error (for example NullPointerException); the compiler does not force handling. Checked ones model expected failures, unchecked ones usually mean a bug.
Does finally always run in Java?
Yes, after a normal finish, a caught or uncaught exception, and even a return inside try or catch. The exceptions are when the JVM itself stops: System.exit(), a crash, or the process being killed. Never put a return in finally, because it overrides the try block's return value or exception.
When should I use try-with-resources?
Whenever you open something that must be closed: files, streams, readers, database connections, sockets, locks wrapped in AutoCloseable. It closes every resource in reverse order even when an exception is thrown, and keeps any close() failure as a suppressed exception instead of losing the original error.

Finish the Java handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.