Exceptions

Java · Lesson 21 of 32 7 min read Updated

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

Java
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");
    }
}
Output
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.

Java
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]);
            }
        }
    }
}
Output
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:

BranchMeaningExamplesMust you handle it?
Errorserious JVM problemsOutOfMemoryError, StackOverflowErrorNo, and usually you cannot
RuntimeException and subclassesprogramming mistakes or bad valuesNullPointerException, IllegalArgumentException, ArithmeticException, NumberFormatExceptionNo (unchecked)
Other Exception subclassesproblems outside your controlIOException, SQLExceptionYes (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:

Java
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);
        }
    }
}
Output
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).

Java
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);
    }
}
Output
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.

Java
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);
    }
}
Output
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 Exception everywhere. 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 (like IOException) makes the child unreachable; the compiler reports error: 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 / catch handles exceptions; finally always runs; multi-catch handles several types at once.
  • Checked exceptions (such as IOException) must be caught or declared with throws; RuntimeExceptions need not be.
  • Use throw with standard exceptions or your own subclasses; keep the original as the cause when wrapping.
  • try-with-resources closes AutoCloseable objects 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
Java
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());
            }
        }
    }
}
Output
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?

2. Which of these is a checked exception?

3. What happens to resources opened in a try-with-resources statement?

4. Where should you start reading a stack trace?

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