IntermediateJava · Lesson 6 of 9

Exceptions

Checked vs unchecked exceptions, try / catch / finally, try-with-resources and custom exceptions.

Exceptions signal errors. Checked exceptions (such as IOException) must be caught or declared with throws — the compiler enforces it. Unchecked exceptions (subclasses of RuntimeException, such as IllegalArgumentException) usually indicate programming errors or invalid input and don't need declaring.

Catch specific exceptions you can handle; let others propagate. Multi-catch (catch (A | B e)) handles several types the same way. finally always runs.

Try-with-resources (try (var reader = ...)) closes files, connections and streams automatically, even when an exception occurs. Define custom exceptions for your domain and keep the original exception as the cause.

Errors.javaJava
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Map;

public class Errors {
    static class StudentNotFoundException extends RuntimeException {
        StudentNotFoundException(String id) { super("Student " + id + " not found"); }
    }

    static class InvalidScoreException extends Exception {          // checked
        InvalidScoreException(String message, Throwable cause) { super(message, cause); }
    }

    static final Map<String, String> STUDENTS = Map.of("S1", "Amina");

    static String findStudent(String id) {
        String name = STUDENTS.get(id);
        if (name == null) throw new StudentNotFoundException(id);
        return name;
    }

    static int parseScore(String text) throws InvalidScoreException {
        try {
            int score = Integer.parseInt(text.trim());
            if (score < 0 || score > 100) throw new InvalidScoreException("Out of range: " + score, null);
            return score;
        } catch (NumberFormatException e) {
            throw new InvalidScoreException("Not a number: '" + text + "'", e);
        }
    }

    public static void main(String[] args) {
        for (String raw : new String[]{"88", "abc", "150"}) {
            try {
                System.out.println("OK " + parseScore(raw));
            } catch (InvalidScoreException e) {
                System.out.println("Invalid: " + e.getMessage()
                    + (e.getCause() != null ? " (cause: " + e.getCause().getClass().getSimpleName() + ")" : ""));
            } finally {
                System.out.println("  checked " + raw);
            }
        }

        try {
            findStudent("S9");
        } catch (StudentNotFoundException e) {
            System.out.println("404: " + e.getMessage());
        }

        String csv = "Amina,88\nJuma,42\n";
        try (BufferedReader reader = new BufferedReader(new StringReader(csv))) {   // closed automatically
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println("Read: " + line);
            }
        } catch (IOException e) {
            System.out.println("Could not read: " + e.getMessage());
        }
    }
}

Key points

  • Checked exceptions must be handled or declared; unchecked ones signal bugs or bad input.
  • Catch only what you can handle, and keep the cause when wrapping exceptions.
  • Use try-with-resources for anything that must be closed.

Exercise

Write parseLine(String line) that turns "Amina,88" into a Result record, throwing a custom checked InvalidLineException for missing commas, blank names or bad scores. Process several lines and report how many failed.

Show solution

Try the exercise yourself first — then compare your approach with this one.

InvalidLineException extends Exception, so it's checked: parseLine must declare it and callers must handle it — the compiler won't let anyone forget. The original NumberFormatException is kept as the cause.

ParseLines.javaJava
import java.util.ArrayList;
import java.util.List;

public class ParseLines {
    record Result(String name, int score) {}

    static class InvalidLineException extends Exception {
        InvalidLineException(String message, Throwable cause) { super(message, cause); }
        InvalidLineException(String message) { this(message, null); }
    }

    static Result parseLine(String line) throws InvalidLineException {
        int comma = line.indexOf(',');
        if (comma < 0) throw new InvalidLineException("missing comma in '" + line + "'");
        String name = line.substring(0, comma).strip();
        if (name.isEmpty()) throw new InvalidLineException("blank name in '" + line + "'");
        try {
            int score = Integer.parseInt(line.substring(comma + 1).strip());
            if (score < 0 || score > 100) throw new InvalidLineException("score out of range in '" + line + "'");
            return new Result(name, score);
        } catch (NumberFormatException e) {
            throw new InvalidLineException("bad score in '" + line + "'", e);
        }
    }

    public static void main(String[] args) {
        List<Result> valid = new ArrayList<>();
        int failed = 0;
        for (String line : List.of("Amina,88", "Juma 42", " ,70", "Neema,abc", "Ali,150", "Rehema, 71")) {
            try {
                valid.add(parseLine(line));
            } catch (InvalidLineException e) {
                failed++;
                System.out.println("Skipped: " + e.getMessage());
            }
        }
        System.out.println(valid);
        System.out.println("Failed: " + failed);   // Failed: 4
    }
}

Check your understanding

  1. What is a checked exception?

  2. What does try-with-resources do?

  3. When does a finally block run?

  4. Why pass the original exception as the cause when wrapping it?

Ask AI