Exceptions
Things go wrong in every real program. A user types nineteen instead of 19, a file is missing, the network drops, someone tries to withdraw more money than they have. When Java hits a problem it cannot handle, it throws an exception: an object that describes what went wrong. If nothing catches it, the program stops and prints a stack trace.
In this lesson you will read stack traces, catch exceptions with try and catch, learn the difference between checked and unchecked exceptions, create your own exception classes and close resources safely with try-with-resources.
What an uncaught exception looks like
public class Crash {
static int parseAge(String text) {
return Integer.parseInt(text);
}
public static void main(String[] args) {
System.out.println("Age: " + parseAge("19"));
System.out.println("Age: " + parseAge("nineteen"));
System.out.println("This line never runs");
}
}Age: 19 Exception in thread "main" java.lang.NumberFormatException: For input string: "nineteen" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) at java.base/java.lang.Integer.parseInt(Integer.java:662) at java.base/java.lang.Integer.parseInt(Integer.java:778) at Crash.parseAge(Crash.java:3) at Crash.main(Crash.java:8)
Learn to read this stack trace; it is your best debugging friend. The first line names the exception class and its message. Below it is the chain of method calls, newest first. Skip the java.base lines (the standard library, whose line numbers differ between Java versions) and find the first line from your code: Crash.parseAge(Crash.java:3), called from main at line 8. That is where to look.
try, catch and finally
Put code that might fail in a try block. If an exception is thrown, Java jumps to the first catch block whose type matches, skipping the rest of the try. A finally block runs every time, whether an exception happened or not, which makes it the place for clean-up.
public class SafeDivide {
public static void main(String[] args) {
String[][] inputs = {{"1200", "4"}, {"1200", "0"}, {"12OO", "4"}};
for (String[] pair : inputs) {
try {
int total = Integer.parseInt(pair[0]);
int people = Integer.parseInt(pair[1]);
System.out.println("Each pays Rs " + total / people);
} catch (NumberFormatException e) {
System.out.println("Not a number: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("Cannot split among 0 people");
} finally {
System.out.println("-- checked " + pair[0] + " / " + pair[1]);
}
}
}
}Each pays Rs 300 -- checked 1200 / 4 Cannot split among 0 people -- checked 1200 / 0 Not a number: For input string: "12OO" -- checked 12OO / 4
The third input has the letter O instead of zeros, a typo that is easy to miss on screen. Because each error is caught, the loop carries on with the next input instead of crashing. e.getMessage() gives the description, and printing e itself gives the class name plus the message.
When two exceptions need the same handling, catch both in one block with a multi-catch: catch (NumberFormatException | ArithmeticException e).
The exception family tree
All exceptions are objects whose classes extend Throwable:
| Branch | Meaning | Examples | Must you handle it? |
|---|---|---|---|
Error | serious JVM problems | OutOfMemoryError, StackOverflowError | No, and usually you cannot |
RuntimeException and subclasses | programming mistakes or bad values | NullPointerException, IllegalArgumentException, ArithmeticException, NumberFormatException | No (unchecked) |
Other Exception subclasses | problems outside your control | IOException, SQLException | Yes (checked) |
For a checked exception, the compiler insists that you either catch it or declare it with throws in the method header, so callers know it can happen. Reading a file is the classic example:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadNotes {
static String loadNotes(String fileName) throws IOException {
return Files.readString(Path.of(fileName));
}
public static void main(String[] args) {
try {
System.out.println(loadNotes("notes-that-do-not-exist.txt"));
} catch (IOException e) {
System.out.println("Could not read notes: " + e);
}
}
}Could not read notes: java.nio.file.NoSuchFileException: notes-that-do-not-exist.txt
loadNotes declares throws IOException and lets the caller decide what to do. If neither main nor loadNotes handled it, the compiler would refuse with error: unreported exception IOException; must be caught or declared to be thrown. NoSuchFileException is a subclass of IOException, so the catch matches it. You will read and write files properly in File I/O with java.nio.file.
Throwing your own exceptions
Use throw to signal a problem yourself. For bad arguments, the standard IllegalArgumentException is usually right. When callers need to react to a specific business problem, create your own exception class by extending Exception (checked) or RuntimeException (unchecked).
public class BankError {
public static void main(String[] args) {
Account acc = new Account("Kabir", 2500);
try {
acc.withdraw(1000);
acc.withdraw(4000);
System.out.println("Not printed");
} catch (InsufficientFundsException e) {
System.out.println("Failed: " + e.getMessage());
System.out.println("Short by Rs " + e.getShortfall());
}
try {
acc.withdraw(-50);
} catch (InsufficientFundsException e) {
System.out.println("Never happens here");
} catch (IllegalArgumentException e) {
System.out.println("Bad amount: " + e.getMessage());
}
System.out.println("Balance: Rs " + acc.getBalance());
}
}
class InsufficientFundsException extends Exception {
private final double shortfall;
InsufficientFundsException(String message, double shortfall) {
super(message);
this.shortfall = shortfall;
}
double getShortfall() {
return shortfall;
}
}
class Account {
private final String owner;
private double balance;
Account(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
double getBalance() { return balance; }
void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive: " + amount);
}
if (amount > balance) {
throw new InsufficientFundsException(
owner + " tried Rs " + amount + " with only Rs " + balance, amount - balance);
}
balance -= amount;
System.out.println("Withdrew Rs " + amount);
}
}Withdrew Rs 1000.0 Failed: Kabir tried Rs 4000.0 with only Rs 1500.0 Short by Rs 2500.0 Bad amount: amount must be positive: -50.0 Balance: Rs 1500.0
A custom exception can carry extra data, like shortfall, that helps the caller respond. Passing the message to super(message) makes getMessage() work. When you catch one exception and throw another, pass the original as the cause: throw new IllegalStateException("Bad port in config", e);. The stack trace then shows both, so the real reason is never lost.
try-with-resources
Files, database connections and network sockets must be closed after use, even when an error happens. Any class that implements AutoCloseable can be opened in the brackets of a try; Java closes it automatically at the end, in reverse order of opening, before any catch runs.
public class Resources {
public static void main(String[] args) {
try (Connection db = new Connection("orders-db");
Connection cache = new Connection("cache")) {
System.out.println("Working with " + db.name + " and " + cache.name);
throw new IllegalStateException("query failed");
} catch (IllegalStateException e) {
System.out.println("Caught: " + e.getMessage());
}
System.out.println("Done");
}
}
class Connection implements AutoCloseable {
final String name;
Connection(String name) {
this.name = name;
System.out.println("Open " + name);
}
@Override
public void close() {
System.out.println("Close " + name);
}
}Open orders-db Open cache Working with orders-db and cache Close cache Close orders-db Caught: query failed Done
Both connections were closed even though the block threw an exception. You will use try-with-resources for files, database connections in JDBC with MySQL, and thread pools.
Common mistakes
- Swallowing exceptions.
catch (Exception e) { }hides the problem. At least log or print it, or let it propagate. - Catching
Exceptioneverywhere. Catch the specific types you can actually handle; let the rest show up as bugs. - Using exceptions for normal flow. Check
if (balance >= amount)rather than relying on a thrown exception for an everyday case. - Wrong order of catch blocks. A catch for a parent type (like
Exception) before a child type (likeIOException) makes the child unreachable; the compiler reportserror: exception IOException has already been caught. - Losing the cause. When wrapping, pass the original exception as the second constructor argument.
Key points
- An exception is an object describing an error; uncaught ones stop the program with a stack trace.
- Read stack traces from the top and look for the first line from your own code.
try/catchhandles exceptions;finallyalways runs; multi-catch handles several types at once.- Checked exceptions (such as
IOException) must be caught or declared withthrows;RuntimeExceptions need not be. - Use
throwwith standard exceptions or your own subclasses; keep the original as the cause when wrapping. - try-with-resources closes
AutoCloseableobjects automatically, in reverse order.
Exercise
Write a method int parseQuantity(String text) for a shopping app. It should strip spaces, convert the text to an int, and throw an IllegalArgumentException with a clear message if the text is not a number (keep the original exception as the cause) or if the quantity is not between 1 and 10. In main, test it with " 3 ", "0", "two" and "12", catching the exception and printing its message for the bad ones.
Show solution
public class Quantity {
static int parseQuantity(String text) {
int qty;
try {
qty = Integer.parseInt(text.strip());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("'" + text + "' is not a number", e);
}
if (qty < 1 || qty > 10) {
throw new IllegalArgumentException("quantity must be 1 to 10, got " + qty);
}
return qty;
}
public static void main(String[] args) {
String[] inputs = {" 3 ", "0", "two", "12"};
for (String in : inputs) {
try {
System.out.println("OK: " + parseQuantity(in));
} catch (IllegalArgumentException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}
}OK: 3 Rejected: quantity must be 1 to 10, got 0 Rejected: 'two' is not a number Rejected: quantity must be 1 to 10, got 12
Callers only need to handle one exception type, IllegalArgumentException, while the original NumberFormatException is still available through getCause() for debugging.
Quick Quiz
1. When does a finally block run?
finally always runs, which makes it suitable for clean-up.
2. Which of these is a checked exception?
IOException extends Exception but not RuntimeException, so the compiler forces you to handle it.
3. What happens to resources opened in a try-with-resources statement?
Try-with-resources calls close() on each resource automatically.
4. Where should you start reading a stack trace?
The top line names the exception; the first frame from your code shows where to look.
Learn Java with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
