Search
Java Exception Handling Tutorial: try-catch-finally, try-with-resources and Custom Exceptions
Sooner or later every Java program meets bad input, a missing file or a network that is down. If you do not plan for it, your program crashes with a long red stack trace, and in a capstone defense that usually happens at the worst possible moment. Exception handling is how Java lets you catch those problems, react to them, and keep the program running or fail with a clear message.
In this Java exception handling tutorial you will learn try, catch and finally, how try-with-resources closes files for you, the difference between checked and unchecked exceptions, how to create custom exceptions with throw and throws, multi-catch, exception chaining, and how to read a stack trace. Every program below was compiled and run on JDK 21, and the output is copied from the terminal.
Requirements
- JDK 11 or newer (JDK 17 or 21 recommended). Check with
java -version. - Any editor or IDE: IntelliJ IDEA, VS Code with the Java extensions, NetBeans or Eclipse.
- Basic Java: classes, methods, loops and arrays.
Since JDK 11 you can run a single source file directly with java FileName.java, without a separate javac step. All example files are in the zip at the bottom of this page.
What Is an Exception?
An exception is an object that Java creates when something goes wrong while the program is running. All exceptions extend Throwable, and the family tree looks like this:
Error: serious problems in the JVM itself, such asOutOfMemoryError. You normally do not catch these.Exception: problems your program can handle.- Checked exceptions, such as
IOExceptionandSQLException. The compiler forces you to catch them or declare them withthrows. They usually come from the outside world: files, databases, networks. - Unchecked exceptions (subclasses of
RuntimeException), such asNullPointerException,ArithmeticException,NumberFormatExceptionandIllegalArgumentException. The compiler does not force you to handle them. They usually mean a bug or invalid input.
- Checked exceptions, such as
Step 1: try, catch and finally
Put the code that might fail inside try. Each catch block handles one type of exception. The finally block runs no matter what. Save this as TryCatchBasics.java and run java TryCatchBasics.java:
- import java.util.List;
- public class TryCatchBasics {
- try {
- int result = 100 / number; // may throw ArithmeticException
- } finally {
- }
- }
- }
- }
Output:
Input: "25" 100 / 25 = 4 (finally always runs) Input: "abc" Not a valid number: For input string: "abc" (finally always runs) Input: "0" Math error: / by zero (finally always runs) Input: "" Not a valid number: For input string: "" (finally always runs) Program finished normally.
Notice that the loop never stops. The bad inputs "abc" and "" throw NumberFormatException, and "0" throws ArithmeticException on the division, but each one is caught and the program moves on to the next input. Without the try block, the first bad value would have crashed the whole program.
When you have several catch blocks, Java checks them from top to bottom and uses the first one that matches. So always put specific exceptions first and general ones like Exception last; the compiler will actually refuse to compile a catch that can never be reached.
Step 2: try-with-resources Closes Things for You
Files, database connections and network sockets must be closed after use, even when an error happens. The old way was a finally block with its own nested try. Since Java 7, try-with-resources does it automatically for any object that implements AutoCloseable. Save this as TryWithResources.java:
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.nio.file.Files;
- import java.nio.file.NoSuchFileException;
- import java.nio.file.Path;
- public class TryWithResources {
- // A tiny resource so we can see exactly when close() is called
- }
- int lines = 0;
- while (reader.readLine() != null) {
- lines++;
- }
- } // reader.close() is called here automatically, even if readLine() throws
- return lines;
- }
- Path file = Path.of("students.txt");
- Files.writeString(file, "Ana\nBen\nCarlo\n");
- try {
- countLines(Path.of("missing.txt"));
- } catch (NoSuchFileException e) {
- }
- db.query("SELECT 1");
- }
- Files.delete(file);
- }
- }
Output:
Lines in students.txt: 3 File not found: missing.txt --- two resources --- open db open cache query db: SELECT 1 close cache close db caught: something failed inside the block
Three things to learn from this output:
countLines()declaresthrows IOExceptioninstead of catching it. That passes the decision to the caller, which is often the right choice for utility methods.- Reading a missing file throws
NoSuchFileException, a subclass ofIOException, so we can catch it specifically and print a friendly message. - With two resources, Java closes them in reverse order (
cachefirst, thendb), and it closes them before thecatchblock runs. Your connections are released even though the block threw an exception.
Rule of thumb: whenever you open something that has a close() method, such as a Scanner on a file, a JDBC Connection, PreparedStatement or ResultSet, open it in a try-with-resources statement.
Step 3: Custom Exceptions with throw and throws
Java's built-in exceptions do not know your business rules. For those, create your own exception class. Extend Exception for a checked exception (the caller must deal with it) or RuntimeException for an unchecked one. Save this as CustomExceptions.java:
- public class CustomExceptions {
- // Checked exception: the caller MUST handle or declare it
- private final double shortBy;
- InsufficientBalanceException(double balance, double amount) {
- this.shortBy = amount - balance;
- }
- double getShortBy() { return shortBy; }
- }
- static class BankAccount {
- private double balance;
- this.owner = owner;
- this.balance = balance;
- }
- void withdraw(double amount) throws InsufficientBalanceException {
- if (amount <= 0) {
- // Unchecked: a programming error, the caller passed a bad value
- }
- if (amount > balance) {
- throw new InsufficientBalanceException(balance, amount);
- }
- balance -= amount;
- }
- }
- BankAccount account = new BankAccount("Maria", 1500.00);
- double[] withdrawals = {500.00, 2000.00, -50.00, 1000.00};
- for (double amount : withdrawals) {
- try {
- account.withdraw(amount);
- } catch (InsufficientBalanceException e) {
- }
- }
- }
- }
Output:
Maria withdrew 500.00, new balance 1000.00 Declined: Cannot withdraw 2000.00; balance is only 1000.00 You are short by 1000.00 Invalid input: Amount must be positive: -50.0 Maria withdrew 1000.00, new balance 0.00
Here is what is going on:
throw(singular) actually creates the problem:throw new InsufficientBalanceException(...).throws(with an s) goes in the method signature and warns callers: "this method can throw this checked exception." If you remove thecatch (InsufficientBalanceException e)block inmain, the code will not compile.- A custom exception can carry extra data. Here
getShortBy()tells the user exactly how much money is missing. - A negative amount is a programming mistake, not a normal business situation, so it throws the unchecked
IllegalArgumentException. This split is a good guideline: checked for situations the caller can reasonably recover from, unchecked for bugs and invalid arguments.
Step 4: Multi-catch and Exception Chaining
When two exceptions need the same handling, you can catch both in one block with |. And when you convert a low-level exception into one that makes sense for your app, pass the original as the cause so the real reason is not lost. Save this as ChainingAndMultiCatch.java:
- import java.io.IOException;
- import java.nio.file.Files;
- import java.nio.file.Path;
- import java.util.List;
- public class ChainingAndMultiCatch {
- // Unchecked custom exception that keeps the original error as its "cause"
- }
- static int readPort(Path file) {
- try {
- if (port < 1 || port > 65535) {
- throw new ConfigException("Port out of range in " + file + ": " + port);
- }
- return port;
- throw new ConfigException("Cannot read port from " + file, e);
- }
- }
- Files.writeString(Path.of("port-good.txt"), " 8080 \n");
- Files.writeString(Path.of("port-typo.txt"), "80a0\n");
- Files.writeString(Path.of("port-range.txt"), "70000\n");
- List<String> files = List.of("port-good.txt", "port-typo.txt", "port-missing.txt", "port-range.txt");
- try {
- } catch (ConfigException e) {
- if (e.getCause() != null) {
- }
- }
- }
- Files.delete(Path.of(name));
- }
- }
- }
Output:
Port: 8080 Cannot read port from port-typo.txt cause: java.lang.NumberFormatException: For input string: "80a0" Cannot read port from port-missing.txt cause: java.nio.file.NoSuchFileException: port-missing.txt Port out of range in port-range.txt: 70000
The calling code only needs to know about ConfigException, but nothing is hidden: getCause() still shows whether the problem was a typo (NumberFormatException) or a missing file (NoSuchFileException). When such an exception is printed with a stack trace, Java shows a Caused by: section for the original error, which makes debugging much easier. Never write throw new ConfigException("Invalid port") inside a catch without passing e; you throw away the most useful information.
Step 5: Read a Stack Trace
When an exception is not caught, Java prints a stack trace and stops. Learning to read it is one of the fastest ways to fix bugs. Save this as StackTraceDemo.java:
- public class StackTraceDemo {
- static double average(int[] scores) {
- int count = scores.length;
- int total = 0;
- for (int score : scores) total += score;
- return (double) total / count;
- }
- }
- }
- }
Output (compiled with javac -g StackTraceDemo.java, then run with java StackTraceDemo):
Average: 86.33 Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "scores" is null at StackTraceDemo.average(StackTraceDemo.java:3) at StackTraceDemo.report(StackTraceDemo.java:10) at StackTraceDemo.main(StackTraceDemo.java:15)
Read it from the top:
- The first line gives the exception type and a message. Since Java 15, "helpful NullPointerException" messages (on by default) tell you exactly what was
null: here, the variablescores. - The first
atline is where it happened: line 3 ofaverage(). - The lines below show how the program got there:
main(line 15) calledreport(line 10), which calledaverage.
So the fix belongs either in main (do not pass null) or in average() (check the argument and throw a clear IllegalArgumentException). If your message shows a placeholder like "<parameter1>" instead of the variable name, the class was compiled without debug information; compile with javac -g (IDEs usually include it already).
Exception Handling Best Practices
- Never swallow exceptions. An empty
catch (Exception e) { }hides bugs. At least log the error, or rethrow it. - Catch the most specific type you can handle. Catching
Exceptioneverywhere also catches bugs you did not expect. - Do not use exceptions for normal flow. Check
if (list.isEmpty())instead of catching an index error. - Validate early. Throw
IllegalArgumentExceptionat the start of a method when the input is invalid, with a message that includes the bad value. - Use try-with-resources for every file, stream, and database object.
- Keep the cause when wrapping exceptions.
- Show friendly messages to users, full details to developers. In a Swing or web app, show "Could not save the record" on screen and write the stack trace to a log.
Conclusion
Exception handling turns crashes into controlled, understandable behavior. Use try/catch for code that can fail, finally or, better, try-with-resources for cleanup, checked custom exceptions for business rules the caller should handle, unchecked exceptions for bugs and bad arguments, and always keep the original cause. Download the examples below, run them, then change the inputs and watch how each catch block reacts. That is the quickest way to make these rules stick.