Skip to main content

08 - Error Handling and Exceptions

Java exception handling: try-catch-finally, checked vs unchecked, throw and throws, custom exceptions and try-with-resources.

What do you think about this article?

At a Glance

Java exception handling: try-catch-finally, checked vs unchecked, throw and throws, custom exceptions and try-with-resources.

Error Handling in Java

Exception handling is fundamental for writing robust code. Java provides a structured mechanism to catch and handle runtime errors without crashing the application.

What You Will Learn

  • Exception hierarchy in Java
  • try-catch-finally
  • Checked vs unchecked exceptions
  • throw and throws
  • Creating custom exceptions
  • try-with-resources
  • Error handling best practices

Exception Hierarchy

Exception Structure
                    Throwable
                    /        \
               Error          Exception
                |                 |
         VirtualMachineError   IOException (checked)
         OutOfMemoryError      SQLException (checked)
         StackOverflowError    RuntimeException (unchecked)
                                      |
                              NullPointerException
                              IllegalArgumentException
                              IndexOutOfBoundsException

Exception Types

Type Description Handling
Error Serious JVM problems Don't handle
Checked Exception Predictable exceptions Mandatory
Unchecked (Runtime) Programming errors Optional

Try-Catch-Finally

Basic Structure
public class GradeHandler {
    public static void main(String[] args) {
        try {
            // Code that may throw exceptions
            int grade = Integer.parseInt("thirty");
            System.out.println("Grade: " + grade);
        } catch (NumberFormatException e) {
            // Exception handling
            System.out.println("Error: grade must be numeric");
            System.out.println("Details: " + e.getMessage());
        } finally {
            // Always executed (even if there's an exception)
            System.out.println("Grade processing complete");
        }
    }
}

Multiple Catches

Multiple Exception Handling
public class StudentRegistry {

    private String[] students = new String[100];
    private int count = 0;

    public void registerStudent(String name, String studentIdStr) {
        try {
            // May throw NumberFormatException
            int studentId = Integer.parseInt(studentIdStr);

            // May throw ArrayIndexOutOfBoundsException
            students[count] = name + " (" + studentId + ")";
            count++;

            System.out.println("Student registered: " + name);

        } catch (NumberFormatException e) {
            System.out.println("Error: invalid student ID");

        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: registry full");

        } catch (Exception e) {
            // Generic catch (must be last)
            System.out.println("Unexpected error: " + e.getMessage());
        }
    }

    // Multi-catch (Java 7+)
    public void registerWithMultiCatch(String name, String studentIdStr) {
        try {
            int studentId = Integer.parseInt(studentIdStr);
            students[count++] = name + " (" + studentId + ")";

        } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
            // Common handling for both exceptions
            System.out.println("Registration error: " + e.getMessage());
        }
    }
}

Throw and Throws

throw throws an exception, throws declares that a method can throw exceptions.

Throwing Exceptions
public class GradeValidator {

    // throws declares that the method may throw IllegalArgumentException
    public void recordGrade(String student, int grade) throws IllegalArgumentException {
        // Validation with throw
        if (student == null || student.trim().isEmpty()) {
            throw new IllegalArgumentException("Student name required");
        }

        if (grade < 60 || grade > 100) {
            throw new IllegalArgumentException(
                "Invalid grade: " + grade + ". Must be between 60 and 100"
            );
        }

        System.out.println(student + " scored " + grade);
    }

    public static void main(String[] args) {
        GradeValidator validator = new GradeValidator();

        try {
            validator.recordGrade("John Smith", 85);
            validator.recordGrade("", 78);  // Will throw exception
        } catch (IllegalArgumentException e) {
            System.out.println("Validation error: " + e.getMessage());
        }
    }
}

Checked vs Unchecked Exceptions

Checked Exception
import java.io.*;

public class FileReader {

    // Checked exception: MUST be declared or handled
    public String readFile(String path) throws IOException {
        BufferedReader reader = new BufferedReader(new java.io.FileReader(path));
        StringBuilder content = new StringBuilder();
        String line;

        while ((line = reader.readLine()) != null) {
            content.append(line).append("\n");
        }

        reader.close();
        return content.toString();
    }

    public static void main(String[] args) {
        FileReader reader = new FileReader();

        // Option 1: Handle with try-catch
        try {
            String content = reader.readFile("grades.txt");
            System.out.println(content);
        } catch (IOException e) {
            System.out.println("Unable to read file: " + e.getMessage());
        }
    }
}
Unchecked Exception
public class AverageCalculator {

    // Unchecked: not required to declare
    public double calculateAverage(int[] grades) {
        // May throw ArithmeticException (division by zero)
        // May throw NullPointerException
        if (grades == null) {
            throw new NullPointerException("Grades array cannot be null");
        }

        if (grades.length == 0) {
            throw new IllegalArgumentException("Grades array empty");
        }

        int sum = 0;
        for (int grade : grades) {
            sum += grade;
        }

        return (double) sum / grades.length;
    }

    public static void main(String[] args) {
        AverageCalculator calc = new AverageCalculator();

        // Optional handling (but recommended)
        try {
            double average = calc.calculateAverage(new int[]{85, 92, 78});
            System.out.println("Average: " + average);

            // This will throw exception
            calc.calculateAverage(null);
        } catch (NullPointerException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

When to Use Checked vs Unchecked

Type When to Use Examples
Checked Recoverable errors, external resources IOException, SQLException
Unchecked Bugs, programming errors NullPointer, IllegalArgument

Custom Exceptions

Custom Checked Exception
// Checked exception (extends Exception)
public class StudentNotFoundException extends Exception {

    private String studentId;

    public StudentNotFoundException(String studentId) {
        super("Student with ID " + studentId + " not found");
        this.studentId = studentId;
    }

    public StudentNotFoundException(String studentId, Throwable cause) {
        super("Student with ID " + studentId + " not found", cause);
        this.studentId = studentId;
    }

    public String getStudentId() {
        return studentId;
    }
}
Custom Unchecked Exception
// Unchecked exception (extends RuntimeException)
public class InvalidGradeException extends RuntimeException {

    private int grade;
    private int minimum;
    private int maximum;

    public InvalidGradeException(int grade, int minimum, int maximum) {
        super(String.format(
            "Grade %d is invalid. Must be between %d and %d",
            grade, minimum, maximum
        ));
        this.grade = grade;
        this.minimum = minimum;
        this.maximum = maximum;
    }

    public int getGrade() { return grade; }
    public int getMinimum() { return minimum; }
    public int getMaximum() { return maximum; }
}
Using Custom Exceptions
import java.util.*;

public class ExamRegistry {

    private Map<String, List<Integer>> studentGrades = new HashMap<>();

    public void enrollStudent(String studentId, String name) {
        studentGrades.put(studentId, new ArrayList<>());
        System.out.println("Enrolled: " + name + " (" + studentId + ")");
    }

    public void recordGrade(String studentId, int grade)
            throws StudentNotFoundException {

        // Verify student
        if (!studentGrades.containsKey(studentId)) {
            throw new StudentNotFoundException(studentId);
        }

        // Verify grade (unchecked - programming error)
        if (grade < 60 || grade > 100) {
            throw new InvalidGradeException(grade, 60, 100);
        }

        studentGrades.get(studentId).add(grade);
        System.out.println("Grade " + grade + " recorded for " + studentId);
    }

    public double getAverage(String studentId) throws StudentNotFoundException {
        if (!studentGrades.containsKey(studentId)) {
            throw new StudentNotFoundException(studentId);
        }

        List<Integer> grades = studentGrades.get(studentId);
        if (grades.isEmpty()) {
            return 0;
        }

        return grades.stream()
                     .mapToInt(Integer::intValue)
                     .average()
                     .orElse(0);
    }

    public static void main(String[] args) {
        ExamRegistry registry = new ExamRegistry();

        registry.enrollStudent("STU001", "John Smith");
        registry.enrollStudent("STU002", "Laura White");

        try {
            registry.recordGrade("STU001", 85);
            registry.recordGrade("STU001", 92);
            registry.recordGrade("STU003", 78);  // Student doesn't exist

        } catch (StudentNotFoundException e) {
            System.out.println("Error: " + e.getMessage());
            System.out.println("Searched ID: " + e.getStudentId());
        }

        try {
            registry.recordGrade("STU002", 105);  // Invalid grade

        } catch (InvalidGradeException e) {
            System.out.println("Grade error: " + e.getMessage());
        } catch (StudentNotFoundException e) {
            System.out.println("Student not found");
        }
    }
}

Try-With-Resources

Introduced in Java 7, it automatically manages closing of resources that implement AutoCloseable.

Before Java 7
// Old way: finally to close
public void readFileOld(String path) {
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(path));
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        System.out.println("Read error: " + e.getMessage());
    } finally {
        // Manual close (verbose and error-prone)
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                System.out.println("Close error: " + e.getMessage());
            }
        }
    }
}
With Try-With-Resources (Java 7+)
import java.io.*;
import java.nio.file.*;

public class FileHandler {

    // Try-with-resources: automatic closing
    public void readFile(String path) {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
        // reader.close() called automatically!
    }

    // Multiple resources
    public void copyFile(String source, String destination) {
        try (
            BufferedReader reader = new BufferedReader(new FileReader(source));
            BufferedWriter writer = new BufferedWriter(new FileWriter(destination))
        ) {
            String line;
            while ((line = reader.readLine()) != null) {
                writer.write(line);
                writer.newLine();
            }
            System.out.println("File copied successfully");

        } catch (IOException e) {
            System.out.println("Copy error: " + e.getMessage());
        }
    }

    // With NIO.2 (more modern)
    public void readFileNio(String path) {
        try {
            String content = Files.readString(Path.of(path));
            System.out.println(content);
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}
Custom AutoCloseable Resource
public class DatabaseConnection implements AutoCloseable {

    private String name;
    private boolean connected;

    public DatabaseConnection(String name) {
        this.name = name;
        this.connected = true;
        System.out.println("Connection opened: " + name);
    }

    public void executeQuery(String query) {
        if (!connected) {
            throw new IllegalStateException("Connection closed");
        }
        System.out.println("Executing: " + query);
    }

    @Override
    public void close() {
        if (connected) {
            connected = false;
            System.out.println("Connection closed: " + name);
        }
    }

    public static void main(String[] args) {
        // Automatic closing guaranteed
        try (DatabaseConnection db = new DatabaseConnection("SchoolDB")) {
            db.executeQuery("SELECT * FROM students");
            db.executeQuery("SELECT * FROM exams");
        }
        // Output:
        // Connection opened: SchoolDB
        // Executing: SELECT * FROM students
        // Executing: SELECT * FROM exams
        // Connection closed: SchoolDB
    }
}

Complete Example: Exam Management System

Domain Exceptions
package school.exceptions;

public class ExamException extends Exception {
    public ExamException(String message) {
        super(message);
    }

    public ExamException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class StudentNotEnrolledException extends ExamException {
    private String studentId;
    private String course;

    public StudentNotEnrolledException(String studentId, String course) {
        super(String.format(
            "Student %s not enrolled in course %s",
            studentId, course
        ));
        this.studentId = studentId;
        this.course = course;
    }

    public String getStudentId() { return studentId; }
    public String getCourse() { return course; }
}

public class ExamAlreadyTakenException extends ExamException {
    private String studentId;
    private String exam;
    private int existingGrade;

    public ExamAlreadyTakenException(String studentId, String exam, int grade) {
        super(String.format(
            "Student %s already took %s with grade %d",
            studentId, exam, grade
        ));
        this.studentId = studentId;
        this.exam = exam;
        this.existingGrade = grade;
    }
}
Exam Management System
package school;

import school.exceptions.*;
import java.util.*;

public class ExamManagement {

    private Map<String, Set<String>> enrollments = new HashMap<>();
    private Map<String, Map<String, Integer>> transcripts = new HashMap<>();

    public void enrollStudent(String studentId, String course) {
        enrollments.computeIfAbsent(course, k -> new HashSet<>())
                   .add(studentId);
        System.out.println(studentId + " enrolled in " + course);
    }

    public void recordExam(String studentId, String course, int grade)
            throws StudentNotEnrolledException,
                   ExamAlreadyTakenException,
                   InvalidGradeException {

        // Verify enrollment
        Set<String> courseStudents = enrollments.get(course);
        if (courseStudents == null || !courseStudents.contains(studentId)) {
            throw new StudentNotEnrolledException(studentId, course);
        }

        // Verify valid grade
        if (grade < 60 || grade > 100) {
            throw new InvalidGradeException(grade, 60, 100);
        }

        // Verify exam not already taken
        String transcriptKey = studentId + "_" + course;
        if (transcripts.containsKey(studentId)) {
            Map<String, Integer> studentExams = transcripts.get(studentId);
            if (studentExams.containsKey(course)) {
                throw new ExamAlreadyTakenException(
                    studentId, course, studentExams.get(course)
                );
            }
        }

        // Record grade
        transcripts.computeIfAbsent(studentId, k -> new HashMap<>())
                   .put(course, grade);
        System.out.println("Exam recorded: " + studentId +
                          " - " + course + ": " + grade);
    }

    public void printTranscript(String studentId) {
        Map<String, Integer> exams = transcripts.get(studentId);
        if (exams == null || exams.isEmpty()) {
            System.out.println("No exams taken for " + studentId);
            return;
        }

        System.out.println("\n=== Transcript for " + studentId + " ===");
        double sum = 0;
        for (Map.Entry<String, Integer> entry : exams.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
            sum += entry.getValue();
        }
        System.out.printf("Average: %.2f%n", sum / exams.size());
    }

    public static void main(String[] args) {
        ExamManagement management = new ExamManagement();

        // Setup
        management.enrollStudent("STU001", "Programming");
        management.enrollStudent("STU001", "Database");
        management.enrollStudent("STU002", "Programming");

        // Record exams with exception handling
        try {
            management.recordExam("STU001", "Programming", 85);
            management.recordExam("STU001", "Database", 92);
            management.recordExam("STU002", "Programming", 78);

            // Intentional errors for testing
            management.recordExam("STU001", "Programming", 88);

        } catch (StudentNotEnrolledException e) {
            System.err.println("ENROLLMENT ERROR: " + e.getMessage());

        } catch (ExamAlreadyTakenException e) {
            System.err.println("DUPLICATE ERROR: " + e.getMessage());

        } catch (InvalidGradeException e) {
            System.err.println("GRADE ERROR: " + e.getMessage());
        }

        // Print transcripts
        management.printTranscript("STU001");
        management.printTranscript("STU002");
    }
}

Best Practices

Golden Rules for Exceptions

  • Don't use exceptions for normal flow: Exceptions are for exceptional cases
  • Specific catches before generic: Order from most specific to most general
  • Don't catch and ignore: At least log the error
  • Use try-with-resources: For resources that need closing
  • Document exceptions: Use @throws in Javadoc
  • Prefer unchecked for programming errors: NullPointer, IllegalArgument
  • Create domain exceptions: For business logic specific errors
  • Include useful context: Clear messages with relevant data
Anti-Patterns to Avoid
// ❌ WRONG: empty catch
try {
    riskyOperation();
} catch (Exception e) {
    // Do nothing = hidden bug!
}

// ❌ WRONG: too generic catch
try {
    riskyOperation();
} catch (Throwable t) {  // Catches Error too!
    // ...
}

// ❌ WRONG: exceptions for flow control
try {
    int i = 0;
    while (true) {
        array[i++].process();  // Waits for IndexOutOfBounds
    }
} catch (IndexOutOfBoundsException e) {
    // End of array
}

// ✅ CORRECT
for (int i = 0; i < array.length; i++) {
    array[i].process();
}

Conclusion

Exception handling is fundamental for robust code. Understanding the hierarchy, using try-with-resources and creating domain exceptions are essential skills.

Key Points to Remember

  • Checked: Recoverable errors, mandatory handling
  • Unchecked: Programming bugs, optional handling
  • try-with-resources: For automatic resource closing
  • Custom exceptions: For domain-specific errors
  • finally: Always executed, useful for cleanup
  • throw: Throws exception, throws: Declares in method

In the next article we'll explore Input/Output in Java: files, streams, NIO.2 and data handling.

Frequently asked questions

What is the purpose of exception handling in Java?

Exception handling is fundamental for writing robust code and prevents applications from crashing due to runtime errors.

How does Java's try-catch-finally block handle exceptions?

The try-catch-finally block provides a structured mechanism to catch and handle runtime errors, with the finally block ensuring that resources are properly closed.

What is the difference between checked and unchecked exceptions in Java?

Checked exceptions must be explicitly handled by the programmer, while unchecked exceptions do not need to be declared or handled.

How does the try-with-resources statement simplify resource management in Java?

The try-with-resources statement automatically manages closing of resources that implement AutoCloseable, eliminating the need for manual resource closure.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects