Exception handling
What an exception is, try-catch-finally, checked versus unchecked, and throwing your own.
Finished reading?
Mark this session so you can track where you are.
What an exception is, try-catch-finally, checked versus unchecked, and throwing your own.
Finished reading?
Mark this session so you can track where you are.
Up to now we have quietly assumed the happy path. The array index was in range, the number you divided by was not zero, the file you opened was really there. Real programs do not get that luxury. This session is about what Java does when something goes wrong while the program is running, and how you take that moment back into your own hands.
try and catch so the program recovers instead of crashing.finally for cleanup that must run no matter what.throw, and declare one with throws.null reference, and the IOException that hovers over every file you read or write. This session is the proper name for all of those, and the tools to deal with them.Start with the failure, because the failure is the whole reason this topic exists. Here is a tiny program that divides two numbers. The second number happens to be zero. In ordinary arithmetic that is undefined, and Java agrees: it has no value to give back, so it refuses to continue.
This is the one example in the session that the playground can really run. Press Run and watch it. The first line prints, then the program stops dead on the division and reports what went wrong. That report is the program throwing an exception with no one there to catch it.
public class Main { public static void main(String[] args) { int total = 100; int people = 0; System.out.println("About to divide..."); int share = total / people; System.out.println("Each person gets " + share); }}ArithmeticException: division by zero
Notice two things. The first println ran, so the program really was alive and moving. And the last println never ran. The moment the division failed, Java abandoned the rest of main on the spot. An exception is exactly this: an object Java creates at the instant something goes wrong at runtime, which interrupts the normal flow and travels back up through the program looking for someone willing to handle it. Find no one, and the program crashes and prints the report you just saw.
An exception is Java's way of signaling “I cannot do what you asked” in the middle of running. Left unhandled, it stops the program where it stands.
Crashing is rarely what you want. A bank that quits because one customer typed a zero is a bad bank. So Java gives you a way to wrap a risky piece of code, attempt it, and if it throws, jump to a recovery plan instead of dying. That is try and catch.
From here on, the playground cannot run these examples. Catching is one of the things our in-browser engine does not interpret, so every snippet below is a read-along block with its real output written underneath. Read each one slowly and trace it in your head. Here is the same division, now caught.
public class Main { public static void main(String[] args) { int total = 100; int people = 0; System.out.println("About to divide..."); try { int share = total / people; System.out.println("Each person gets " + share); } catch (ArithmeticException e) { System.out.println("Could not divide: " + e.getMessage()); } System.out.println("Program keeps going."); }}About to divide... Could not divide: division by zero Program keeps going.
Read the shape of it. The try block holds the code that might fail. If everything inside it succeeds, the catch block is skipped entirely. But if any line inside try throws, Java stops that block immediately and looks at the catch. Here the division throws an ArithmeticException, and the catch says it handles exactly that type, so control jumps inside it. The line that would have printed the share never runs, just like before, but now the program does not crash. It recovers and carries on to the last line.
The (ArithmeticException e) after catch looks like a method parameter, and it works like one. When the exception is thrown, Java hands the exception object to your catch block under the name you chose, here e. That object carries useful information. The most common thing to ask it is e.getMessage(), which returns the human-readable text describing what went wrong. You will meet e.printStackTrace() later in this session too.
catch (Exception e) to catch absolutely everything, and sometimes that is right. But naming the specific type you expect, ArithmeticException here, documents what you think can go wrong and stops you from silently swallowing a different, surprising failure you never meant to hide. Be as specific as you honestly can.catch (Exception e) { } with nothing inside, just to make the compiler stop complaining about a checked exception. It compiles, and it feels like progress, but you have built a trap: the program now fails silently. The exception is caught and thrown away, no message, no clue, and later, when something downstream is wrong, you will have no idea a failure ever happened. At the very least print or log the exception. An empty catch hides exactly the information you will be desperate for later.Often there is something you must do whether the risky code succeeded or failed: close a file, release a connection, switch a sign back off. Putting it after the try is not safe, because if the catch itself returns or re-throws, that line might be skipped. Java gives you a third block for exactly this. finally runs no matter how the try ended.
public class Main { public static void main(String[] args) { System.out.println("Opening the report file..."); try { System.out.println("Wrote one line."); } catch (RuntimeException e) { System.out.println("Something failed: " + e.getMessage()); } finally { System.out.println("Closing the file. (this always runs)"); } System.out.println("Done."); }}Opening the report file... Wrote one line. Closing the file. (this always runs) Done.
Here nothing failed, so the catch was skipped, but the finally still ran. Now watch what happens when the try does throw. The finally still runs, between the failure and the recovery message, every single time.
public class Main { public static void main(String[] args) { System.out.println("Opening the report file..."); try { int bad = 10 / 0; System.out.println("This line never runs."); } catch (ArithmeticException e) { System.out.println("Something failed: " + e.getMessage()); } finally { System.out.println("Closing the file. (this always runs)"); } System.out.println("Done."); }}Opening the report file... Closing the file. (this always runs) Something failed: / by zero Done.
Trace the order carefully, because it surprises people. The division throws. Java leaves the try at once. Before it can run the matching catch, it runs the finally, so “Closing the file” prints first. Only then does the catch body run and print the recovery message. The rule is simple to say and worth memorizing: finally always runs, and it runs on the way out, after the try or catch has done its part.
tryholds the risky work.catchis the recovery plan for when it fails.finallyis the cleanup that happens either way. The first is required, the other two are each optional, but you need at least one of them.
finally is the old, careful pattern, and it is good to see it once so you understand what the newer shortcut does for you. When you reach reading files in earnest, you will use try-with-resources, a form of try that closes the file for you automatically. It is built on exactly the finally idea you just saw.Not all exceptions are treated the same by the compiler. Java sorts them into two families, and the difference decides whether you are forced to deal with one before your code will even compile.
An unchecked exception is one the compiler does not require you to handle. These are the runtime mistakes you have already met: dividing by zero, a bad array index, a null reference. They all descend from a type called RuntimeException. The compiler lets your code compile without a single try around them, on the theory that they signal bugs you should fix rather than failures you must routinely expect.
A checked exception is one the compiler insists you account for. The classic example is IOException, the failure that hangs over every file operation. A file can be missing, locked, or full, and none of that is your bug, it is just the world being unreliable. Because such failure is expected, Java makes you promise, in writing, that you have either handled it or passed it on. Forget to, and the program will not compile.
import java.io.FileReader; public class Main { public static void main(String[] args) { // FileReader can throw a checked IOException. // With no try/catch and no throws, this will NOT compile: FileReader r = new FileReader("data.txt"); }}error: unreported exception IOException; must be caught or declared to be thrown
FileReader r = new FileReader("data.txt");
^That is not a crash at runtime. It is the compiler refusing the program before it ever runs, pointing straight at the line and telling you a checked exception is going unhandled. The fix is either to wrap it in a try/catch or to declare it with throws, which we meet next.
ArithmeticException, NullPointerException, ArrayIndexOutOfBoundsExceptionIOException, FileNotFoundExceptionSo far exceptions have been thrown for you, by the division operator or the array index. You can also throw one deliberately. This is how you guard your own methods against being asked to do something nonsensical. If a withdrawal amount is negative, that is not a calculation you should quietly perform, it is a caller's mistake, and saying so loudly is kinder than carrying on.
The keyword is throw, followed by a new exception object. Here a method refuses a negative amount by throwing an IllegalArgumentException, an unchecked exception that means “you passed me an argument I cannot accept”.
public class Main { public static void main(String[] args) { int balance = 100; System.out.println("Withdrawing 30..."); balance = withdraw(balance, 30); System.out.println("Balance is now " + balance); System.out.println("Withdrawing -5..."); try { balance = withdraw(balance, -5); } catch (IllegalArgumentException e) { System.out.println("Caught: " + e.getMessage()); } } static int withdraw(int balance, int amount) { if (amount < 0) { throw new IllegalArgumentException("amount cannot be negative"); } return balance - amount; }}Withdrawing 30... Balance is now 70 Withdrawing -5... Caught: amount cannot be negative
Follow the two calls. The first, withdraw(balance, 30), passes the check and returns 70, which prints. The second, withdraw(balance, -5), hits the if, runs throw, and the method ends right there without returning anything. The exception flies out of withdraw, back into main, where the surrounding try catches it and prints the message you wrote into the exception. The text you passed to the constructor is exactly what getMessage() later returns.
throw (no s) is an action: it raises an exception right now, in the body of a method. throws (with an s) is a label on a method's signature: it warns callers that this method might let a certain exception escape. We turn to throws next.Catching is not the only response to a checked exception. Sometimes the method you are writing is not the right place to recover. A low-level method that reads a file has no idea what the program as a whole should do when the file is missing. Rather than guess, it can decline to handle the failure and let it travel up to a caller who does know. The way it says so is throws on its signature.
import java.io.FileReader;import java.io.IOException; public class Main { public static void main(String[] args) { System.out.println("Trying to read config..."); try { loadConfig(); } catch (IOException e) { System.out.println("Could not read config: " + e.getMessage()); System.out.println("Using default settings instead."); } } // This method does NOT handle the IOException itself. // It declares throws, passing the responsibility up to main. static void loadConfig() throws IOException { FileReader r = new FileReader("data.txt"); // ... read the file ... }}Trying to read config... Could not read config: data.txt (No such file or directory) Using default settings instead.
Read how the responsibility moves. loadConfig opens a file, which can throw a checked IOException. It chooses not to catch it. By writing throws IOExceptionafter its parameter list, it makes a promise to its callers: “I might let this exception out, so you must be ready for it.” That promise is what makes the compiler happy. Up in main, the call to loadConfig is wrapped in a try, so when the file is genuinely missing, the exception lands there and main recovers with sensible defaults.
For any exception you face, you have two honest choices: handle it here with
try/catch, or declarethrowsand pass it to whoever called you. For checked exceptions the compiler makes you pick one. You cannot simply ignore it.
When an exception is never caught, Java prints a stack trace before the program dies. It looks intimidating, a wall of indented lines, but it is one of the most useful things in all of debugging once you know how to read it. It tells you what went wrong and the exact path of method calls that led there.
Here is a program that crashes on purpose. A chain of calls, main calls start which calls compute, and the deepest one divides by zero.
public class Main { public static void main(String[] args) { start(); } static void start() { compute(); } static void compute() { int x = 5 / 0; }}Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.compute(Main.java:11)
at Main.start(Main.java:7)
at Main.main(Main.java:3)Read it from the top down, because the top is where the truth is.
ArithmeticException: / by zero. That is what went wrong. Always read this line first.at, is where it went wrong: Main.compute(Main.java:11). The failure happened inside compute, at line 11 of the file. This is almost always the line you want to look at.at line below it is the caller of the one above. compute was called by start, which was called by main. Reading downward walks you back out through the chain of calls, all the way to where the program began.So a stack trace is a story told backwards. The top is the scene of the failure; each line beneath is who sent you there. When you are debugging, your eye should go to the first line for the kind of error, then to the topmost at line that names your file for the exact spot to open and fix.
catch block you can call e.printStackTrace() to print the very same report while the program keeps running. It is one of the fastest ways to learn what path led to a failure you caught. We go deeper on reading traces, and the IDE tools that jump you straight to the line, in annotations, docs, stack traces, and the IDE.Here is one slightly larger example that uses every piece in one place: a checked exception passed up with throws, a try/catch that handles it, a thrown IllegalArgumentException of our own, and a finally that always cleans up.
import java.io.IOException; public class Main { public static void main(String[] args) { System.out.println("Starting save..."); try { save(""); } catch (IllegalArgumentException e) { System.out.println("Refusing to save: " + e.getMessage()); } catch (IOException e) { System.out.println("Disk problem: " + e.getMessage()); } finally { System.out.println("Closing writer. (always)"); } System.out.println("Save attempt finished."); } static void save(String fileName) throws IOException { if (fileName.isEmpty()) { throw new IllegalArgumentException("file name must not be empty"); } // ... real file writing would go here and could throw IOException ... }}Starting save... Refusing to save: file name must not be empty Closing writer. (always) Save attempt finished.
Trace it once through. save("") is called with an empty name. Inside, the if is true, so throw raises an IllegalArgumentException and save ends. The exception reaches main, where the first matching catch handles it and prints the refusal. On the way out, the finally runs and prints the cleanup line. Then the program continues past the whole structure to the final message. Two catch blocks, and Java picked the one whose type matched what was actually thrown.
Try each one yourself first, then open the answer.
finally example where the try divided 10 / 0. The output was four lines. Why did “Closing the file” print before “Something failed”?NullPointerException, IOException, ArrayIndexOutOfBoundsException, FileNotFoundException.throw and throws? Use a sentence for each.java.lang.NullPointerException at Main.parse(Main.java:22) at Main.run(Main.java:11)Take these away. They continue exactly what we just did.
divide(int a, int b) that returns a / b, but throws an IllegalArgumentException with a clear message when b is zero, instead of letting the ArithmeticException happen. In main, call it once with a valid pair and once with a zero divisor, catching and printing the message for the second. Write out by hand exactly what it prints.readFirstLine(String name) with throws IOException on its signature (you do not need real file code, just the signature and a comment where reading would go). In main, call it inside a try/catch (IOException e) and a finally that prints “cleanup done”. Explain in one sentence why main must catch it.try anywhere, that crashes with an ArrayIndexOutOfBoundsException from three methods deep (main calls a calls b, and b indexes past an array). On paper, sketch the stack trace you expect: the error line first, then the three at lines in the right order.IOException as your concrete example, and say what you would do instead if the language did not force you.