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
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"); } } }
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
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()); } } }
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
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()); } } }
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
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); } } } }
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.
VIP25 gives 25% off Unknown coupon: FREE100