Project Based Learning in Java

Java Exception Handling

try, catch, nested handling, finally, resources, throw, throws, and propagation.

Lecture 1.3.2 CO3 aligned Unit 1

Learning outcome: Handle exceptions with try, catch, and finally, use try-with-resources, and distinguish throw from throws.

Exception handling separates normal work from failure recovery. A good handler catches only failures it understands and either recovers, reports useful context, or propagates the problem. The compiler's checked/unchecked classification determines whether handling must be written; it does not determine whether recovery will succeed.

try and catch

Place code that may fail inside try. A compatible catch block handles the exception.

In simple terms: try is the tightrope and catch is the safety net. Code runs normally on the wire; if it falls, the matching net catches it instead of the whole show stopping.

public class ParseDemo {
    public static void main(String[] args) {
        try {
            int age = Integer.parseInt("twenty");
            System.out.println(age);
        } catch (NumberFormatException exception) {
            System.out.println("Age must be a whole number.");
        }
    }
}

Statements after the failing line inside try are skipped. Execution resumes after the matching handler.

Multiple catch Blocks

Use separate handlers when different failures need different responses. Write more specific exception types before broader ones.

In simple terms: Hang the nets narrowest first. A wide net placed at the front catches everything, and the specific nets behind it never see a thing — which is why Java rejects that order outright.

public class ScoreLookup {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("Usage: java ScoreLookup <index>");
            return;
        }

        int[] scores = {70, 80, 90};

        try {
            int index = Integer.parseInt(args[0]);
            System.out.println(scores[index]);
        } catch (NumberFormatException exception) {
            System.out.println("Index must be a whole number.");
        } catch (ArrayIndexOutOfBoundsException exception) {
            System.out.println("Index must be from 0 to " + (scores.length - 1) + ".");
        }
    }
}

A multi-catch handles several types in the same way. For example, the two handlers in the preceding program could be replaced by one shared handler:

try {
    int index = Integer.parseInt(args[0]);
    System.out.println(scores[index]);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException exception) {
    System.out.println("Invalid index input: " + exception.getMessage());
}

The alternatives in a multi-catch cannot have a parent-child relationship, and the catch parameter is implicitly final within the handler.

Nested try-catch

A try block may appear inside another try block. The inner handler should deal with a local, specific failure; the outer handler can deal with a broader failure in the surrounding operation.

public class NestedTryDemo {
    public static void main(String[] args) {
        try {
            int first = Integer.parseInt(args[0]);
            int second = Integer.parseInt(args[1]);

            try {
                System.out.println("Quotient: " + first / second);
            } catch (ArithmeticException exception) {
                System.out.println("The divisor must not be zero.");
            }
        } catch (ArrayIndexOutOfBoundsException exception) {
            System.out.println("Supply two integer arguments.");
        } catch (NumberFormatException exception) {
            System.out.println("Both arguments must be valid integers.");
        }
    }
}

The inner block focuses on division, while the outer block validates command-line input. Nesting is useful when the inner operation has a meaningful local recovery. Excessive nesting reduces readability; separate methods are preferable when the operations are logically independent.

finally

The finally block normally runs when control leaves try or catch, whether completion is normal or caused by return, break, or an exception. It is not guaranteed if the process or JVM terminates abruptly, for example through System.exit, a crash, or forced termination.

In simple terms: finally is locking up at closing time. Whether the day went well, went badly, or you left early, the door still gets locked.

try {
    System.out.println("Working");
} catch (RuntimeException exception) {
    System.out.println("Failed");
} finally {
    System.out.println("Cleanup step");
}

Historically, finally was used to close resources. For closable resources, try-with-resources is safer.

Try-with-Resources

A resource declared in the try header is closed automatically. The resource must implement AutoCloseable.

In simple terms: A resource declared in the try header is a library book on automatic return. You cannot forget to close it, because the closing is written into the borrowing.

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

public class FirstLineReader {
    static String readFirstLine(Path path) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(path)) {
            return reader.readLine();
        }
    }
}

This remains safe if reading throws an exception. Resources are closed in reverse order of declaration. If the body and close() both throw, the body exception is propagated and the closing failure is available through getSuppressed().

For application exceptions, a checked exception such as IOException must be caught or declared with throws; an unchecked runtime exception such as IllegalArgumentException does not have this compile-time requirement. The Error family is also unchecked, but applications do not normally treat errors as routine recoverable exceptions.

throw

throw transfers control by throwing one existing throwable object at a particular point. In the common form below, new creates the object and throw throws it.

static void setPercentage(int value) {
    if (value < 0 || value > 100) {
        throw new IllegalArgumentException("Percentage must be 0 to 100");
    }
}

The keyword is followed by an exception object, and the statement ends with a semicolon.

throws

throws appears in a method declaration. It lists throwable types the method may propagate. The declaration is required for checked exceptions that can escape and optional for unchecked types.

static String load(Path path) throws IOException {
    return Files.readString(path);
}
Keyword Location Purpose
throw Inside a method or block Throws one exception object now
throws In a method declaration Declares types that may be propagated to the caller

Declaring throws does not handle an exception. For a checked type, the caller must catch it or declare it further; for an unchecked type, the compiler imposes no such requirement.

Propagation Example

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

public class ConfigLoader {
    static String readConfig(Path path) throws IOException {
        return Files.readString(path);
    }

    public static void main(String[] args) {
        try {
            System.out.println(readConfig(Path.of("config.txt")));
        } catch (IOException exception) {
            System.out.println("Could not load configuration: " + exception.getMessage());
        }
    }
}

Handler Design Guidelines

  1. Catch the most specific useful exception type.
  2. Do not silently ignore exceptions.
  3. Add context that helps the user or developer act.
  4. Preserve the original cause when wrapping an exception.
  5. Use finally or try-with-resources for dependable cleanup.
  6. Do not use exceptions for ordinary loop or branch control.

Common Mistakes

Mistake Correction
Placing catch (Exception e) before a specific catch Put specific handlers first.
Leaving a catch block empty Recover, report, or rethrow with context.
Assuming throws handles an exception It only declares propagation.
Manually closing an AutoCloseable resource in many branches Prefer try-with-resources.
Returning from finally Avoid it; it can hide exceptions and earlier return values.
Saying throw creates an exception new creates an object; throw throws the object.
Assuming throws is allowed only for checked types Any throwable type may be declared, but only checked types are subject to catch-or-declare.

Practice

  1. Read two command-line integers and handle invalid input.
  2. Use try-with-resources to read the first line of a text file.
  3. Write validateAge(int age) that throws IllegalArgumentException for a negative value.
  4. Trace an exception through three methods until main catches it.
  5. Use a nested try-catch to validate two arguments and handle division by zero locally.

Quick Check

1. Does finally run only when an exception occurs?

No. It runs whether the try block completes normally or an exception occurs, except in unusual cases such as the JVM shutting down.

2. What is the difference between throw and throws?

throw throws an exception object from code. throws declares in a method signature which exception types may be propagated.

3. Why is try-with-resources preferred for files?

It closes resources automatically even when an operation fails, reducing leaks and cleanup errors.

4. When is a nested try-catch reasonable?

It is reasonable when an inner operation has a specific local recovery while the surrounding operation needs separate, broader handling.

5. Does throw new IllegalArgumentException(...) use one keyword or two?

Two. new creates the exception object, and throw throws that object.

Summary

Use try for work that may throw, catch for specific recovery, and finally for cleanup that normally must run. Nested handling can isolate a local failure when used sparingly. Try-with-resources is the preferred way to manage closable resources. new creates an exception object, throw throws it, and throws declares possible propagation to a caller.