A try-catch block is how Java lets a program react to an exception instead of crashing. The risky code goes in try, and a catch clause runs only when something goes wrong. This lesson covers the syntax, the exact way control flows when an exception is thrown, what the exception object offers once it's caught, the scope rules that often surprise on first encounter, and what nested try-catch looks like in practice.
The minimal try-catch block has two parts: a try block holding the code that might fail, and a catch clause that names the exception type it can handle. The shape looks like this:
The try block holds one or more statements. The catch clause declares a single parameter, just like a method parameter. The type of that parameter says which exceptions this catch is willing to handle, and the name (e here, by convention) is the variable used to refer to the exception object inside the catch body.
Here's a real example. A storefront wants to convert a price typed in by a customer into a double. The input might be junk, so Double.parseDouble could throw NumberFormatException.
The string "29.99" parses cleanly, so the try block runs to completion and the catch clause is skipped entirely. The program prints the parsed price and exits.
Now run the same program with bad input:
Double.parseDouble("twenty") throws NumberFormatException. Java looks at the surrounding try block, sees a catch for NumberFormatException, and routes control there. The line System.out.println("Parsed price: $" + price); is never reached, because once a throw happens, the rest of the try block is skipped.
The catch didn't fix the input. It didn't recover the original number. It made a choice about what to do when parsing failed, and it printed a friendly message instead of letting the program crash with a stack trace. That's the job of a catch clause: decide how the code should respond to this particular failure.
Here are the flow rules, line by line:
When the program reaches a try block, it starts running the statements inside, one after another, like any other block of code. There are two ways the block can end:
catch clause is skipped completely. Execution continues with the statement that follows the entire try-catch construct.The key word in option 2 is "abandoned". Lines that would have run after the throw don't run. Variables that hadn't been assigned yet stay unassigned. Side effects that hadn't happened yet don't happen.
A diagram makes this easier to see:
Every branch in the diagram matters. The "no match" path on the right is the easy one to forget. If the throw doesn't match the catch parameter type, the catch doesn't run. The exception keeps traveling up the call stack, looking for a handler somewhere higher up.
A small program makes the "abandoned statements" rule concrete. The cart has three line items, and the program tries to compute the average price per item by dividing the total by the item count. If the cart is empty, the division throws ArithmeticException.
That code won't throw on a double division by zero. Java's floating-point math returns Infinity rather than throwing. To get a real ArithmeticException, integer division is needed.
Three things stand out. First, "Before division" printed because that statement ran before the throw. Second, both "Average (cents): ..." and "After division" are missing from the output. Those statements were abandoned the moment cartTotalCents / itemCount threw. Third, "Program continues." still printed, because the catch handled the exception, which means execution continues normally after the try-catch.
Removing the try-catch entirely and letting the same division throw, the program prints "Before division" and then crashes with a stack trace. The lines after the throw still don't run, but now there's no continuation either, because nothing caught the exception and the JVM terminates the thread.
A catch clause only catches exceptions whose type matches the parameter type or one of its subtypes. Mismatched types fly past the catch as if it weren't there.
This program tries to look up a product by index, then runs a catch that's not relevant to the actual failure:
The throw is an ArrayIndexOutOfBoundsException, and the catch is looking for NumberFormatException. The two types are unrelated, so the catch is skipped and the exception propagates up to the JVM, which prints the stack trace and ends the program. The output looks identical to having no try-catch at all, because as far as this exception is concerned, there wasn't one.
Fixing it is a one-line change:
Now the parameter type matches the thrown type, the catch body runs, and the message replaces the stack trace.
One important detail: the parameter type is matched by subtype, not by exact equality. A catch (Exception e) would also catch the ArrayIndexOutOfBoundsException above, because ArrayIndexOutOfBoundsException is a subclass of RuntimeException, which is a subclass of Exception. Writing the catch this broadly is usually a mistake; the reason comes up later.
The exception object passed to the catch is a real Java object, with methods to call. Three are useful most of the time.
getMessage() returns a short string the exception was constructed with. It's the same string that appears after the exception type in a stack trace. For a parsing failure, the message tends to include the input that was rejected, which is exactly the detail useful for logging or for a friendlier display.
The detail line includes the rejected input, which makes the failure obvious to anyone reading the logs.
getClass().getSimpleName() returns the unqualified class name of the exception. It's useful for logging what kind of failure happened without dumping a full stack trace, and without writing out the package name every time.
The first line names the exception type. The second line names the specific problem. Together they give a log reader almost everything they need.
printStackTrace() prints the full stack trace, including the exception type, the message, and the chain of method calls that led to the throw. It's the same output Java would print if the exception went uncaught, but the placement is under explicit control.
printStackTrace writes to standard error by default, not standard output, so it can interleave with System.out lines in unexpected order on some consoles. In production code, send exceptions to a proper logger rather than directly to standard error, but for learning and debugging, printStackTrace is the fastest way to see what happened.
Building a stack trace is not free. The JVM captures the call stack when the exception is constructed, which costs more than a normal return. Avoid throwing exceptions for ordinary control flow, like deciding which branch of an if to take.
A variable declared inside the try block lives only inside that block. The catch clause can't see it, and neither can the code that follows the try-catch. This rule looks unintuitive at first when writing something simple.
This code does not compile:
The compiler error looks like this:
The variable price was declared inside the try block. The try block ends at the closing brace before catch. Once execution leaves the block, price no longer exists. The compiler isn't being picky. It's enforcing the same scope rules that apply to any other block of code in Java: a variable lives only inside the innermost set of braces it was declared in.
The fix is to declare price outside the try block and assign to it inside.
Now price lives in the surrounding method scope, the try block updates it on success, and the print after the try-catch can read it. If parsing had failed, price would still hold its initial value of 0.0, which is why initializing it with a sensible default matters. Without the initial value, the compiler would have refused to print price after the try-catch, because the path through the catch leaves price unassigned.
The same scope rule applies to the catch parameter. The variable e lives only inside the catch body. Code after the try-catch can't reach e, which is fine because e has no useful meaning once the catch has finished handling the failure.
One subtlety: if the try block runs to completion (no throw), Java still treats variables it declared as out of scope after the block. The "in scope only inside the block" rule doesn't care whether the block ran normally or threw. The brace boundary is what matters.
A try block can contain another try block. The inner one handles failures from its own body. Anything it doesn't catch keeps traveling outward, and the outer try-catch gets a chance to handle it. The pattern is useful when two different operations in the same flow can each fail in their own way and need separate handling.
Consider an example. A storefront receives two pieces of customer input: a quantity and a price. Both come in as strings. Each one parses independently, and a failure in one shouldn't look like a failure in the other.
The inner catch handles the price failure with a specific message. The outer try block had already finished its own risky line (parsing the quantity) by the time the inner failure happened, so the outer catch never runs. Control falls past both try-catches and continues with "Done.".
Now flip the inputs so the quantity is bad:
The inner try never even started, because the throw came from Integer.parseInt(quantityInput), which is in the outer try block. The outer catch handled it. The flow shows that nested try-catch isn't magic: each layer handles whatever falls inside its own braces, and a failure in an outer line never reaches an inner catch because the inner block hasn't started yet.
A catch clause can also contain its own try, which sometimes makes sense if recovering from one failure involves a second risky step. The shape is the same.
The outer catch runs because the primary input was junk. Inside the outer catch, a second try-catch attempts the fallback, which succeeds. The inner catch never runs because the fallback parses cleanly. The inner exception variable is named inner instead of e because the outer catch's e is still in scope inside the outer catch body, and reusing the name would be a compile error.
Nest only as deep as needed. Two levels can be useful. Three or four levels usually means the code is doing too much in one place, and breaking the work into helper methods will untangle the flow.
Some code catches Exception or even Throwable directly. The shape looks tempting because it catches everything in one line:
This compiles and runs, but it's almost always the wrong call. The catch swallows every kind of failure, including programming bugs like NullPointerException and ArrayIndexOutOfBoundsException that should usually surface as errors rather than hide behind a friendly message. The catch also makes the code harder to maintain, because the next reader can't tell which failures the author actually anticipated and which ones got caught by accident.
The fix is to name the specific exception types the code is prepared to handle, like NumberFormatException in the parsing example above. Treat catch (Exception e) as a smell. If it appears, identify what can actually go wrong and write a catch for that specific thing instead.
A small program that exercises a single try with one catch, the exception object's methods, and a nested try-catch for a fallback step.
Walk through the flow. Integer.parseInt("10") succeeds and stores 10 in slot. Then cart[10] throws ArrayIndexOutOfBoundsException, abandoning the rest of the try block. The catch matches the type, so its body runs. The body prints the failure type and message, then opens a nested try-catch that reads cart[0]. That access succeeds and prints the fallback. After the nested try-catch finishes, the outer catch ends. Control falls past both try-catches and prints "Lookup finished.".
With an empty cart (String[] cart = new String[0];), the fallback access would also throw ArrayIndexOutOfBoundsException, the inner catch would match, and the program would print "Cart is empty, no fallback available." followed by "Lookup finished.". The outer catch handler doesn't need to think about that case, because the inner one already does.
10 quizzes