Custom Exceptions & try-with-resources

Built-in exceptions cover general problems, but your application has its own failure cases: an order that can't be shipped, a coupon that has expired. Custom exception classes make those explicit. And whenever you open something that must be closed — files, network connections, database connections — try-with-resources guarantees it's closed, even when things go wrong.

Defining an exception

JAVA
class InsufficientStockException extends RuntimeException {
    private final String product;
    private final int requested;
    private final int available;

    InsufficientStockException(String product, int requested, int available) {
        super("Cannot reserve " + requested + " x " + product + ", only " + available + " left");
        this.product = product;
        this.requested = requested;
        this.available = available;
    }

    int shortfall() { return requested - available; }
}

class Warehouse {
    private int keyboards = 2;

    void reserve(int qty) {
        if (qty > keyboards) throw new InsufficientStockException("keyboard", qty, keyboards);
        keyboards -= qty;
    }
}

public class Main {
    public static void main(String[] args) {
        Warehouse w = new Warehouse();
        try {
            w.reserve(5);
        } catch (InsufficientStockException e) {
            System.out.println(e.getMessage());
            System.out.println("Order " + e.shortfall() + " more");
        }
    }
}
Output
Cannot reserve 5 x keyboard, only 2 left
Order 3 more

Extend RuntimeException for an unchecked exception or Exception for a checked one. Pass a helpful message to super(…), and add fields for any details the handler might need. Name exceptions after the problem, ending in Exception.

Checked or unchecked?

Use a checked exception when the caller can reasonably be expected to recover and you want the compiler to make sure they think about it. Use an unchecked exception for programming errors and for failures callers usually can't handle locally. Modern libraries and frameworks lean towards unchecked exceptions.

Wrapping exceptions: the cause

JAVA
class ConfigException extends RuntimeException {
    ConfigException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class Main {
    static int loadPort(String value) {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new ConfigException("Invalid port setting: " + value, e);
        }
    }

    public static void main(String[] args) {
        try {
            loadPort("80a");
        } catch (ConfigException e) {
            System.out.println(e.getMessage());
            System.out.println("Caused by: " + e.getCause());
        }
    }
}
Output
Invalid port setting: 80a
Caused by: java.lang.NumberFormatException: For input string: "80a"

When translating a low-level exception into a higher-level one, pass the original as the cause. Stack traces then show both ("Caused by: …"), so the root problem isn't lost.

try-with-resources

JAVA
class Connection implements AutoCloseable {
    private final String name;

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

    void send(String msg) {
        if (msg.isEmpty()) throw new IllegalArgumentException("empty message");
        System.out.println(name + " <- " + msg);
    }

    @Override
    public void close() {
        System.out.println("close " + name);
    }
}

public class Main {
    public static void main(String[] args) {
        try (Connection c = new Connection("db")) {
            c.send("SELECT 1");
        }

        try (Connection a = new Connection("A"); Connection b = new Connection("B")) {
            b.send("");
        } catch (IllegalArgumentException e) {
            System.out.println("error: " + e.getMessage());
        }
    }
}
Output
open db
db <- SELECT 1
close db
open A
open B
close B
close A
error: empty message

Resources declared in the parentheses after try are closed automatically when the block ends — normally or by exception — in reverse order of opening, before any catch runs. Any class implementing AutoCloseable works: files, streams, sockets, database connections.

Reading a file safely

JAVA
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

public class Main {
    public static void main(String[] args) throws IOException {
        String data = "first line\nsecond line\n";
        try (BufferedReader reader = new BufferedReader(new StringReader(data))) {
            String line;
            int n = 1;
            while ((line = reader.readLine()) != null) {
                System.out.println(n++ + ": " + line);
            }
        }
    }
}
Output
1: first line
2: second line

The same pattern reads real files with Files.newBufferedReader(path) (lesson 51). Without try-with-resources, you'd need a finally block with its own null checks and nested try — easy to get wrong.

Suppressed exceptions

If the body throws and then close() throws too, the body's exception is kept and the closing one is attached to it as "suppressed" (e.getSuppressed()), so neither is silently lost.

Try it yourself

Write a validation method that throws a custom checked exception, and handle it.

Try it yourself · Main.java
Edit the code, copy it into Main.java and run java Main.java (Java 25+). How to set up Java
Output
VIP25 gives 25% off
Unknown coupon: FREE100