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.
Related deep dive: 07 - Collections Framework: List, Set e Map.
Related deep dive: 09 - Input/Output e NIO.2.
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.
Commenti
Caricamento commenti...
Sign in to comment