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
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
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
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.
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
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());
}
}
}
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
// 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;
}
}
// 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; }
}
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.
// 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());
}
}
}
}
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());
}
}
}
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
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;
}
}
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
// ❌ 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.







