Java Exception Handling Tutorial with Runnable Examples

Learn how Java moves control through try, catch, and finally, then build checked custom exceptions and safer file handling with runnable code.

KnowledgeGate Team

Exam prep & CS education

Updated 5 Nov 20266 min read
Java Exception Handling Tutorial with Runnable Examples

A program may work for expected input, then stop when it receives "seven", divides by zero, or cannot find a file. Java exception handling redirects control through try, catch, finally, throw, throws, custom exceptions, and try-with-resources. For a focused treatment of checked versus unchecked rules and finally return traps, use Exception Handling in Java: Checked vs Unchecked, finally Traps and Output Questions. DivideDemo traces catch selection, WithdrawalDemo carries a checked domain exception, and ScoreReaderDemo closes its reader automatically.

What an exception is and how Java classifies it

An exception is an object representing a problem during execution. On the normal path, Java completes the current statement and moves to the next one. On the exceptional path, it creates or propagates an exception object until it finds a compatible handler. During debugging, inspect its type, message, and method-call stack trace.

Keep Error separate. A blanket catch (Throwable) can hide serious failures the application cannot handle.

Use two practical groups:

  • A checked exception such as IOException must be caught or declared with throws.

  • An unchecked exception such as NumberFormatException or ArithmeticException need not be caught or declared.

Catch matching follows inheritance. catch (RuntimeException e) matches its subclass NumberFormatException, but the specific catch usually supports a clearer response.

Follow try, catch, and finally in a runnable example

Save this as DivideDemo.java:

java
public class DivideDemo {
    public static void main(String[] args) {
        int numerator = 84;
        String divisorText = "0";

        try {
            int divisor = Integer.parseInt(divisorText);
            int quotient = numerator / divisor;
            System.out.println("Quotient: " + quotient);
        } catch (NumberFormatException e) {
            System.out.println("Not a whole number: " + divisorText);
        } catch (ArithmeticException e) {
            System.out.println("Cannot divide " + numerator + " by zero");
        } finally {
            System.out.println("Attempt complete.");
        }
    }
}

With divisorText = "0", parsing succeeds, division throws ArithmeticException, and the output is:

Code
Cannot divide 84 by zero
Attempt complete.

Change the text to "7". The calculation is 84 / 7 = 12, so Java prints Quotient: 12 and then Attempt complete. Change it to "seven", and parsing fails before division. Java prints Not a whole number: seven and then Attempt complete. Only the first compatible catch runs. The finally block runs after both the normal path and these handled-exception paths.

Catch where you can add context, choose a fallback, request input, or terminate cleanly. Do not catch and continue without a meaningful response.

Build a checked custom exception

This program separates invalid input from a valid request the account cannot satisfy. Save it as WithdrawalDemo.java:

java
class InsufficientBalanceException extends Exception {
    private final int shortfall;

    InsufficientBalanceException(int shortfall) {
        super("insufficient balance");
        this.shortfall = shortfall;
    }

    int getShortfall() {
        return shortfall;
    }
}

class BankAccount {
    private int balance = 5000;

    void withdraw(int amount) throws InsufficientBalanceException {
        if (amount <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
        if (amount > balance) {
            throw new InsufficientBalanceException(amount - balance);
        }
        balance -= amount;
    }

    int getBalance() {
        return balance;
    }
}

public class WithdrawalDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        int[] attempts = {1250, 6200, -50};

        for (int amount : attempts) {
            try {
                account.withdraw(amount);
                System.out.println("Withdrew " + amount);
            } catch (InsufficientBalanceException e) {
                System.out.println("Declined: short by " + e.getShortfall());
            } catch (IllegalArgumentException e) {
                System.out.println("Invalid amount: " + amount);
            } finally {
                System.out.println("Balance now: " + account.getBalance());
            }
        }
    }
}

The first withdrawal leaves 5000 - 1250 = 3750. The second has shortfall 6200 - 3750 = 2450 and does not change the balance. The negative amount is rejected before any subtraction. The exact output is:

Code
Withdrew 1250
Balance now: 3750
Declined: short by 2450
Balance now: 3750
Invalid amount: -50
Balance now: 3750

throw creates and sends one exception at that point. throws declares that a method may pass a checked exception to its caller. The custom type carries shortfall = 2450 as structured data, so the caller does not have to parse a message.

Three-lane control-flow figure for WithdrawalDemo shows balances 5000 and 3750, amounts 1250, 6200, and -50, with success, shortfall, and invalid-input paths.

Use try-with-resources for closeable resources

Save this as ScoreReaderDemo.java. Place scores.txt beside it with 42 on the first line:

java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class ScoreReaderDemo {
    public static void main(String[] args) {
        try (BufferedReader reader =
                 Files.newBufferedReader(Path.of("scores.txt"))) {
            int score = Integer.parseInt(reader.readLine());
            System.out.println("Score: " + score);
        } catch (IOException e) {
            System.out.println("Could not read scores.txt");
        } catch (NumberFormatException e) {
            System.out.println("scores.txt must start with an integer");
        }
    }
}

The valid file prints Score: 42. A missing file reaches the IOException catch; forty-two reaches the NumberFormatException catch. An opened reader closes automatically after success or failure. Keep finally for small actions such as the balance log; use try-with-resources for reliable closing.

Scores flow shows opened readers closing automatically; a missing file fails before any reader exists.

Avoid exception-handling mistakes that hide bugs

An empty catch hides both the failure and its location. If Integer.parseInt("12A") fails, reject 12A with a clear message or propagate the exception with useful context. Do not use exceptions for an expected boolean decision, and do not return from finally, because that return can replace an earlier result or exception.

Order catches from specific to broad. Save this example as CatchOrderDemo.java. The ordering keeps the IOException response reachable and preserves its cause:

java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CatchOrderDemo {
    public static void main(String[] args) {
        try {
            Files.readString(Path.of("order-1042.txt"));
        } catch (IOException e) {
            throw new IllegalStateException("Cannot load order 1042", e);
        } catch (Exception e) {
            System.out.println("Unexpected problem");
        }
    }
}

Reversing the catches makes the later IOException catch unreachable because Exception already matches it. Passing e into the replacement exception preserves the original trace. For a related Java value-object idea, revise the String handling article.

Practise output tracing and compiler diagnosis

Place each of the next two snippets inside a main method. First predict this output:

java
int zero = 0;
try {
    System.out.print("A");
    int x = 8 / zero;
} catch (ArithmeticException e) {
    System.out.print("B");
} finally {
    System.out.print("C");
}
System.out.print("D");

It prints ABCD. Java prints A, division throws before the assignment completes, the catch prints B, finally prints C, and execution resumes after the handled exception to print D.

Next, Integer.parseInt("24x") throws NumberFormatException. A multi-catch can show the matched runtime type:

java
try {
    Integer.parseInt("24x");
} catch (NumberFormatException | ArithmeticException e) {
    System.out.print(e.getClass().getSimpleName());
} finally {
    System.out.print(":done");
}

The output is NumberFormatException:done. For a compiler question, suppose a method calls Files.readString(Path.of("marks.txt")) but has neither catch (IOException e) nor throws IOException. The diagnosis is an unhandled checked exception, not a runtime output. If an assessment names a language version or syntax subset, confirm its rules in the assessment owner's current official instructions.

Exercises, the short version, and your next step

Try these without copying the earlier programs:

  1. Parse {"18", "x", "24"}. Produce valid-value sum 18 + 24 = 42 and rejected-count 1.

  2. Validate age 15 against minimum 18 with a checked AgeOutOfRangeException. Print Age 15 is below minimum 18.

  3. Read marks.txt containing 72, 88, bad, and 95. Report invalid line 3; the valid sum is 72 + 88 + 95 = 255, and the average is 255 / 3 = 85.0. Use try-with-resources.

Use these rules:

  • Catch only an exception you can handle meaningfully.

  • Order catches from the most specific type to the broadest.

  • Use throw for the event and throws for the method contract.

  • Create a custom exception for a meaningful domain failure.

  • Use try-with-resources for closeable resources.

Continue the language sequence with the Java course. Move to DSA using Java after your language basics are stable, or browse the broader Coding & Skills category. Run all three DivideDemo input variants, then complete Exercise 1 before moving on.