Java try...catch

Su Suriya Ravichandran Updated 14 Nov 2026
10 min read ·Lesson 124 of 133

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
}
PartPurpose
try { ... }Marks the protected region — the statements Java watches for exceptions
catchIntroduces a handler
(ExceptionType name)The type of exception this handler accepts, and a variable that receives the exception object
{ ... } after catchWhat 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 the try block runs. The catch block is skipped entirely, and execution continues after the whole statement.
  • Exception ("abc"). Integer.parseInt throws NumberFormatException. Java abandons the rest of the try block immediately — "quantity = ..." and "end of try block" are never printed, and qty is never assigned. Java then looks at the catch clauses, finds one whose type matches, and runs it. After the catch block finishes, execution continues normally after the statement.

Two things to understand from this:

  1. Execution never returns into the try block after an exception. If you need to try again, you must loop (shown later in this lesson).
  2. Once the exception is caught, it is considered handled. The program continues as if nothing had happened — which is why the catch block 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:

HandlerAlso 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
MethodReturnsUse it for
getMessage()The detail message, or nullShort 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 classDiagnostics
getStackTrace()An array of StackTraceElement (top frame first)Tools and custom reporting
printStackTrace()Prints the full trace to System.errQuick 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:

  1. The try is 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 the try wrapped the entire loop, the first bad row would end the whole import.
  2. A missing field is checked with if, not caught. The number of fields is easy to test before using them, so an if is clearer and cheaper than letting f[2] throw ArrayIndexOutOfBoundsException. 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.
  3. Only NumberFormatException is caught. A NullPointerException from a programming mistake would still surface instead of being reported as a "bad row".
  4. The Line is added only on success. valid.add(line) sits after the parsing inside the try, so it is skipped automatically when parsing fails.
  5. 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 a return leaves the method.
  • "twenty" makes parseInt throw; the catch prints a message and the loop runs again.
  • -5 parses fine, but it's not a valid age. That is a business rule, not a parsing failure, so it is checked with if and continue.
  • 30 passes both checks, and return age exits the loop and the method.
  • Reading the whole line with nextLine() and then parsing it avoids a known Scanner problem: when nextInt() fails, the invalid token stays in the input, and a loop that calls nextInt() 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 if can prevent the problem (checking list.isEmpty(), index < array.length, obj != null);
  • the exception indicates a bug. Fix the code instead of catching NullPointerException or ArrayIndexOutOfBoundsException;
  • you can't handle it at the current level. Let it propagate to a caller that can — the throws keyword 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.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Java try...catch and get answers drawn from it.

Signed-in readers only.