Java try...catch
The try...catch Statement
A try...catch statement lets a program handle an exception instead of crashing. You put code that might fail inside a try block, and code that should run when a particular exception occurs inside a catch block.
try {
// code that might throw an exception
} catch (ExceptionType name) {
// code that runs only if ExceptionType (or a subclass) was thrown in the try block
}
| Part | Purpose |
|---|---|
try { ... } | Marks the protected region — the statements Java watches for exceptions |
catch | Introduces a handler |
(ExceptionType name) | The type of exception this handler accepts, and a variable that receives the exception object |
{ ... } after catch | What to do about it — recover, use a default, report, retry |
Both blocks need braces, even for a single statement. A try can't stand alone: it must be followed by at least one catch or a finally block (covered in the Finally Block lesson), otherwise the code doesn't compile.
Why use it: without a handler, an exception propagates up and, in main, ends the program. A catch block stops that propagation at a point where your code knows how to respond — for example, telling a user their input was invalid and asking again.
How Control Flows Through try...catch
public class Flow {
public static void main(String[] args) {
String[] inputs = {"25", "abc"};
for (String input : inputs) {
System.out.println("Parsing \"" + input + "\"");
try {
int qty = Integer.parseInt(input);
System.out.println(" quantity = " + qty);
System.out.println(" end of try block");
} catch (NumberFormatException e) {
System.out.println(" catch: " + e.getMessage());
}
System.out.println(" after try/catch");
}
}
}
Output:
Parsing "25"
quantity = 25
end of try block
after try/catch
Parsing "abc"
catch: For input string: "abc"
after try/catch
The two iterations show the two possible paths:
- No exception (
"25"). Every statement in thetryblock runs. Thecatchblock is skipped entirely, and execution continues after the whole statement. - Exception (
"abc").Integer.parseIntthrowsNumberFormatException. Java abandons the rest of thetryblock immediately —"quantity = ..."and"end of try block"are never printed, andqtyis never assigned. Java then looks at thecatchclauses, finds one whose type matches, and runs it. After thecatchblock finishes, execution continues normally after the statement.
Two things to understand from this:
- Execution never returns into the
tryblock after an exception. If you need to try again, you must loop (shown later in this lesson). - Once the exception is caught, it is considered handled. The program continues as if nothing had happened — which is why the
catchblock must leave the program in a sensible state.
Matching: Which Exceptions a catch Handles
A catch (T e) block handles an exception if the thrown object is an instance of T or any subclass of T. This follows the class hierarchy from the Exception Basics lesson:
| Handler | Also handles |
|---|---|
catch (NumberFormatException e) | only NumberFormatException (it has no common subclasses) |
catch (IllegalArgumentException e) | NumberFormatException and other illegal-argument subclasses |
catch (RuntimeException e) | every unchecked exception |
catch (Exception e) | every checked and unchecked exception (but not Error) |
If the thrown exception doesn't match any catch, the statement doesn't handle it: the exception keeps propagating to the caller exactly as if there were no try at all.
Catch the most specific type you can actually handle. catch (NumberFormatException e) says "I know how to deal with bad numeric text." catch (Exception e) says "I'll deal with anything," including a NullPointerException caused by a bug, which usually you can't meaningfully deal with.
Using the Exception Object
The variable in the catch clause holds the exception object, which has useful methods:
public class Info {
public static void main(String[] args) {
try {
int[] seats = new int[4];
seats[4] = 1;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("getMessage(): " + e.getMessage());
System.out.println("toString(): " + e);
System.out.println("class: " + e.getClass().getName());
StackTraceElement top = e.getStackTrace()[0];
System.out.println("thrown at: " + top.getMethodName() + " line " + top.getLineNumber());
}
}
}
Output:
getMessage(): Index 4 out of bounds for length 4
toString(): java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
class: java.lang.ArrayIndexOutOfBoundsException
thrown at: main line 5
| Method | Returns | Use it for |
|---|---|---|
getMessage() | The detail message, or null | Short explanations, validation feedback |
toString() | Class name + ": " + message (just the class name if there's no message) | Log lines — it never loses the type |
getClass().getName() | The exception's class | Diagnostics |
getStackTrace() | An array of StackTraceElement (top frame first) | Tools and custom reporting |
printStackTrace() | Prints the full trace to System.err | Quick debugging in small programs |
In production code, pass the exception to a logging framework instead of printing it — for example log.error("Import failed", e) with SLF4J — so the full stack trace is written to the application's logs with a timestamp and context.
The variable name e is only a convention. Its scope is the catch block: it isn't visible after the block ends.
Variables Declared Inside try
A variable declared inside a try block exists only inside that block:
public class Scope {
public static void main(String[] args) {
try {
int port = Integer.parseInt("80x");
} catch (NumberFormatException e) {
System.out.println("bad port");
}
System.out.println(port);
}
}
Compiler output:
Scope.java:8: error: cannot find symbol
System.out.println(port);
^
symbol: variable port
location: class Scope
1 error
This is a deliberate rule. If the exception happened before port was assigned, there would be no valid value to print. To use the result after the statement, declare the variable before the try and make sure every path assigns it:
int port;
try {
port = Integer.parseInt(text);
} catch (NumberFormatException e) {
port = 8080; // default when the configured value is invalid
}
System.out.println("Listening on " + port);
The compiler accepts this because both paths — success and failure — assign port. Remove the assignment from the catch and you'll get variable port might not have been initialized.
A cleaner alternative is to move the logic into a method that returns from inside the try:
static int parsePort(String text, int defaultPort) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
return defaultPort;
}
}
Practical Example: Importing CSV Order Lines
A data import must not stop at the first bad row. It should import what it can and report what it couldn't.
import java.util.*;
public class Orders {
record Line(String sku, int qty, double price) { }
public static void main(String[] args) {
List<String> csv = List.of(
"PEN-01,10,12.50",
"NB-22,three,45.00",
"BAG-07,2",
"INK-03,5,80.00");
List<Line> valid = new ArrayList<>();
List<String> errors = new ArrayList<>();
for (int row = 0; row < csv.size(); row++) {
String[] f = csv.get(row).split(",");
if (f.length != 3) {
errors.add("row " + (row + 1) + ": expected 3 fields but found " + f.length);
continue;
}
try {
Line line = new Line(f[0], Integer.parseInt(f[1]), Double.parseDouble(f[2]));
valid.add(line);
} catch (NumberFormatException e) {
errors.add("row " + (row + 1) + ": invalid number - " + e.getMessage());
}
}
double total = 0;
for (Line l : valid) total += l.qty() * l.price();
System.out.println("Imported " + valid.size() + " lines, total = " + total);
errors.forEach(System.out::println);
}
}
Output:
Imported 2 lines, total = 525.0
row 2: invalid number - For input string: "three"
row 3: expected 3 fields but found 2
How it works:
- The
tryis inside the loop. Each row gets its own protected region, so a failure affects only that row and the loop continues with the next. If thetrywrapped the entire loop, the first bad row would end the whole import. - A missing field is checked with
if, not caught. The number of fields is easy to test before using them, so anifis clearer and cheaper than lettingf[2]throwArrayIndexOutOfBoundsException. Use exceptions for problems you can't easily check in advance — whether"three"is a valid number is easiest to find out by trying to parse it. - Only
NumberFormatExceptionis caught. ANullPointerExceptionfrom a programming mistake would still surface instead of being reported as a "bad row". - The
Lineis added only on success.valid.add(line)sits after the parsing inside thetry, so it is skipped automatically when parsing fails. - The total is
10 × 12.50 + 5 × 80.00 = 525.0.
Practical Example: Asking Again Until the Input Is Valid
Because execution never goes back into a try block, retrying requires a loop. This is the standard pattern for console input. The Scanner here reads from a string so the example produces the same output every time; in a real program it would be new Scanner(System.in).
import java.util.Scanner;
public class Retry {
public static void main(String[] args) {
Scanner in = new Scanner("twenty\n-5\n30\n");
int age = readAge(in);
System.out.println("Age accepted: " + age);
}
static int readAge(Scanner in) {
while (true) {
System.out.print("Enter age: ");
String text = in.nextLine().trim();
System.out.println(text); // echo, because input comes from a string here
try {
int age = Integer.parseInt(text);
if (age < 0 || age > 130) {
System.out.println(" Age must be between 0 and 130.");
continue;
}
return age;
} catch (NumberFormatException e) {
System.out.println(" \"" + text + "\" is not a whole number.");
}
}
}
}
Output:
Enter age: twenty
"twenty" is not a whole number.
Enter age: -5
Age must be between 0 and 130.
Enter age: 30
Age accepted: 30
while (true)repeats until areturnleaves the method."twenty"makesparseIntthrow; thecatchprints a message and the loop runs again.-5parses fine, but it's not a valid age. That is a business rule, not a parsing failure, so it is checked withifandcontinue.30passes both checks, andreturn ageexits the loop and the method.- Reading the whole line with
nextLine()and then parsing it avoids a knownScannerproblem: whennextInt()fails, the invalid token stays in the input, and a loop that callsnextInt()again would fail forever.
Swallowing Exceptions
An empty catch block hides problems. Consider this method:
public class Swallow {
static int discountPercent(String code) {
int percent = 0;
try {
percent = Integer.parseInt(code.substring(4));
} catch (Exception e) {
// ignored
}
return percent;
}
public static void main(String[] args) {
System.out.println(discountPercent("SALE15"));
System.out.println(discountPercent("SALE"));
System.out.println(discountPercent(null));
System.out.println(discountPercent("SALEfifteen"));
}
}
Output:
15
0
0
0
Three very different situations — an empty suffix, a null code (a bug in the caller) and a non-numeric suffix — all produce a silent 0. A customer gets no discount and nobody knows why; a null bug elsewhere is never discovered. The combination of catch (Exception e) and an empty block is the worst case, because it hides every kind of failure.
A better version catches only the expected failure, keeps the null bug visible, and makes the outcome explicit:
static OptionalInt discountPercent(String code) {
try {
return OptionalInt.of(Integer.parseInt(code.substring(4)));
} catch (NumberFormatException e) {
return OptionalInt.empty(); // "SALE" and "SALEfifteen": no valid number
}
}
Now "SALE15" gives OptionalInt[15], invalid codes give OptionalInt.empty, and null still throws a NullPointerException that points straight to the caller's bug. When ignoring an exception really is correct, say so in a comment explaining why, so the next developer knows it was deliberate.
When to Use try...catch
Use it when:
- the failure depends on something outside your code — user input, files, network, databases, other services;
- you can do something meaningful in the handler: a default value, a retry, a clear error message, skipping one record;
- an API forces it, as with checked exceptions such as
IOException.
Avoid it when:
- a simple
ifcan prevent the problem (checkinglist.isEmpty(),index < array.length,obj != null); - the exception indicates a bug. Fix the code instead of catching
NullPointerExceptionorArrayIndexOutOfBoundsException; - you can't handle it at the current level. Let it propagate to a caller that can — the
throwskeyword lesson shows how to declare this for checked exceptions.
Exceptions are also relatively expensive to create, because the JVM records the stack trace. Using them in a hot loop for normal control flow (for example, to end iteration) is slower and harder to read than ordinary conditions.
A single try can have several catch blocks for different exception types, or one block handling several types at once. That is the topic of the next lesson, Multiple Catch.