Master Java exception handling with try-catch-finally, checked versus unchecked exceptions, multi-catch, try-with-resources, and designing custom exception hierarchies for robust production code.
Production Java code fails. Network connections time out, database queries fail, users provide invalid input, and files are missing. The question is not whether failures happen — it is whether your code handles them gracefully or crashes with a cryptic stack trace.
Java's exception handling forces you to think about failure at the API design level. Checked exceptions appear in method signatures and require callers to handle them explicitly. Unchecked exceptions signal programming errors (null pointer dereference, out-of-bounds access) that should have been prevented. Understanding this distinction, combined with proper resource management and exception chaining, is what separates production-quality code from prototype code.
In this lesson you will learn the complete exception hierarchy, write robust error handling with try-with-resources (which eliminates resource leaks), design custom exceptions that carry useful context, and apply the best-practice rules that experienced Java teams enforce in code reviews.
1Throwable
2├── Error — JVM problems (OutOfMemoryError, StackOverflowError)
3│ └── ... — DO NOT catch in application code
4└── Exception
5 ├── Checked exceptions (must handle or declare)
6 │ ├── IOException
7 │ │ ├── FileNotFoundException
8 │ │ └── SocketException
9 │ ├── SQLException
10 │ ├── ClassNotFoundException
11 │ └── ... (any Exception NOT extending RuntimeException)
12 └── RuntimeException (unchecked — no forced handling)
13 ├── NullPointerException
14 ├── ArrayIndexOutOfBoundsException
15 ├── ClassCastException
16 ├── NumberFormatException
17 ├── IllegalArgumentException
18 ├── IllegalStateException
19 └── ...1// Checked exception: MUST declare with 'throws' or catch
2public String readFile(String path) throws IOException { // declared
3 return Files.readString(Path.of(path));
4}
5
6// If you catch it, you don't need throws
7public String readFileSafe(String path) {
8 try {
9 return Files.readString(Path.of(path));
10 } catch (IOException e) {
11 return "Error: " + e.getMessage();
12 }
13}
14
15// Unchecked exception: can throw without declaring
16public int divide(int a, int b) {
17 if (b == 0) throw new ArithmeticException("Divisor cannot be zero");
18 return a / b;
19}
20
21// IllegalArgumentException: caller passed bad data
22public void setAge(int age) {
23 if (age < 0 || age > 150) {
24 throw new IllegalArgumentException(
25 "Age must be 0-150, was: " + age);
26 }
27 this.age = age;
28}
29
30// IllegalStateException: method called in wrong state
31public void start() {
32 if (running) throw new IllegalStateException("Already running");
33 running = true;
34}1// Basic try-catch-finally
2try {
3 int result = Integer.parseInt("abc"); // throws NumberFormatException
4 System.out.println(result);
5} catch (NumberFormatException e) {
6 System.out.println("Not a number: " + e.getMessage());
7} finally {
8 // ALWAYS runs, even if exception is thrown or return is executed
9 System.out.println("This always runs");
10}
11
12// Catching specific exceptions first (most specific to most general)
13try {
14 processFile(path);
15} catch (FileNotFoundException e) { // most specific
16 System.err.println("File not found: " + path);
17} catch (IOException e) { // more general
18 System.err.println("IO error: " + e.getMessage());
19} catch (Exception e) { // most general — last resort
20 System.err.println("Unexpected: " + e.getMessage());
21 throw new RuntimeException("Processing failed", e); // re-throw with context
22}
23
24// Multi-catch (Java 7+): handle multiple exception types the same way
25try {
26 processData(input);
27} catch (IllegalArgumentException | NumberFormatException e) {
28 // Both handled identically — 'e' is effectively final
29 System.err.println("Invalid input: " + e.getMessage());
30 return Collections.emptyList();
31}
32
33// ANTI-PATTERN: never catch and silently ignore
34try {
35 processFile(path);
36} catch (IOException e) {
37 // ← WRONG: swallowing exception hides bugs
38}
39
40// CORRECT: at minimum, log it
41try {
42 processFile(path);
43} catch (IOException e) {
44 logger.error("Failed to process file: {}", path, e);
45 throw new ServiceException("File processing failed", e); // or handle
46}1// Pre-Java 7: manual resource management was error-prone
2InputStream in = null;
3try {
4 in = new FileInputStream("data.txt");
5 // process...
6} catch (IOException e) {
7 // handle
8} finally {
9 if (in != null) {
10 try { in.close(); } // close() itself can throw!
11 catch (IOException e) { /* suppress? log? */ }
12 }
13}
14
15// Java 7+: try-with-resources (implements AutoCloseable)
16// close() is called AUTOMATICALLY when the block exits
17try (InputStream in = new FileInputStream("data.txt")) {
18 byte[] data = in.readAllBytes();
19 return new String(data, StandardCharsets.UTF_8);
20} catch (IOException e) {
21 throw new RuntimeException("Cannot read file", e);
22}
23// 'in' is closed here regardless of success or exception
24
25// Multiple resources: closed in REVERSE order of declaration
26try (
27 Connection conn = dataSource.getConnection();
28 PreparedStatement stmt = conn.prepareStatement(sql);
29 ResultSet rs = stmt.executeQuery()
30) {
31 while (rs.next()) {
32 process(rs.getString(1));
33 }
34} catch (SQLException e) {
35 throw new RepositoryException("Query failed", e);
36}
37// rs closed first, then stmt, then conn
38
39// Any class implementing AutoCloseable works
40public class Timer implements AutoCloseable {
41 private final long start = System.nanoTime();
42 private final String name;
43
44 public Timer(String name) { this.name = name; }
45
46 @Override
47 public void close() {
48 long elapsed = System.nanoTime() - start;
49 System.out.printf("%s took %d ms%n", name, elapsed / 1_000_000);
50 }
51}
52
53try (Timer t = new Timer("database query")) {
54 // performs query
55} // prints timing automatically1// Custom checked exception: for expected error conditions
2public class InsufficientFundsException extends Exception {
3 private final double requested;
4 private final double available;
5
6 public InsufficientFundsException(double requested, double available) {
7 super(String.format(
8 "Insufficient funds: requested %.2f, available %.2f",
9 requested, available));
10 this.requested = requested;
11 this.available = available;
12 }
13
14 // Getters for programmatic access to error details
15 public double getRequested() { return requested; }
16 public double getAvailable() { return available; }
17 public double getShortfall() { return requested - available; }
18}
19
20// Custom unchecked exception: for programming errors or unrecoverable states
21public class RepositoryException extends RuntimeException {
22 private final String operation;
23
24 public RepositoryException(String message, String operation, Throwable cause) {
25 super(message, cause); // cause enables exception chaining
26 this.operation = operation;
27 }
28
29 public String getOperation() { return operation; }
30}
31
32// Exception chaining: preserve the original cause when re-throwing
33public User findUser(long id) {
34 try {
35 return jdbcTemplate.queryForObject(
36 "SELECT * FROM users WHERE id = ?",
37 userMapper, id);
38 } catch (EmptyResultDataAccessException e) {
39 // SQLException details are preserved in the cause chain
40 throw new RepositoryException(
41 "User not found: " + id, "findUser", e);
42 } catch (DataAccessException e) {
43 throw new RepositoryException(
44 "Database error finding user: " + id, "findUser", e);
45 }
46}
47// Callers see RepositoryException but can inspect e.getCause()
48// for the underlying SQLException details1// 1. Handle exceptions at the right layer
2// Repository: translates DB exceptions to domain exceptions
3public class UserRepository {
4 public User findById(long id) {
5 try {
6 return jdbc.queryForObject(sql, mapper, id);
7 } catch (SQLException e) {
8 throw new RepositoryException("DB error", e); // translate
9 }
10 }
11}
12// Service: catches domain exceptions, applies business logic
13public class UserService {
14 public UserDto getUser(long id) {
15 try {
16 return toDto(repository.findById(id));
17 } catch (RepositoryException e) {
18 throw new ServiceException("User " + id + " not found", e);
19 }
20 }
21}
22// Controller: converts to HTTP responses
23
24// 2. Use Optional instead of null for absence
25public Optional<User> findUserOptional(long id) {
26 try {
27 return Optional.of(repository.findById(id));
28 } catch (RepositoryException e) {
29 return Optional.empty(); // absence is not an error
30 }
31}
32
33// 3. Validate early with preconditions
34public void transfer(Account from, Account to, double amount) {
35 Objects.requireNonNull(from, "Source account cannot be null");
36 Objects.requireNonNull(to, "Destination account cannot be null");
37 if (amount <= 0) throw new IllegalArgumentException("Amount must be positive: " + amount);
38 // ... rest of transfer logic
39}
40
41// 4. Document exceptions in Javadoc
42/**
43 * Withdraws the specified amount from the account.
44 *
45 * @throws InsufficientFundsException if balance is less than amount
46 * @throws IllegalArgumentException if amount is <= 0
47 */
48public void withdraw(double amount) throws InsufficientFundsException { ... }
49
50// 5. Never catch Throwable or Error (except in very specific framework code)
51// WRONG:
52catch (Throwable t) { ... } // catches OutOfMemoryError — unrecoverable!
53
54// CORRECT:
55catch (Exception e) { ... } // application exceptions onlyA complete file processing pipeline demonstrating try-with-resources, custom exceptions, exception chaining, multi-catch, and proper logging boundaries.
1import java.io.*;
2import java.nio.file.*;
3import java.util.*;
4
5/**
6 * FileProcessor.java — robust file processing with comprehensive exception handling.
7 */
8
9class FileProcessingException extends RuntimeException {
10 private final Path file;
11 private final String operation;
12
13 public FileProcessingException(String message, Path file, String operation, Throwable cause) {
14 super(message, cause);
15 this.file = file;
16 this.operation = operation;
17 }
18
19 public Path getFile() { return file; }
20 public String getOperation() { return operation; }
21}
22
23class ParseException extends Exception {
24 private final int lineNumber;
25 private final String line;
26
27 public ParseException(String message, int lineNumber, String line) {
28 super(message);
29 this.lineNumber = lineNumber;
30 this.line = line;
31 }
32
33 public int getLineNumber() { return lineNumber; }
34 public String getLine() { return line; }
35}
36
37record Student(String name, int age, double gpa) {}
38
39public class FileProcessor {
40
41 /**
42 * Parses a CSV file into Student records.
43 * @throws FileProcessingException if file cannot be read
44 */
45 public static List<Student> parseStudentCsv(Path csvPath) {
46 List<Student> students = new ArrayList<>();
47 List<String> errors = new ArrayList<>();
48
49 // try-with-resources: BufferedReader closed automatically
50 try (BufferedReader reader = Files.newBufferedReader(csvPath)) {
51 String line;
52 int lineNum = 0;
53
54 while ((line = reader.readLine()) != null) {
55 lineNum++;
56 if (lineNum == 1) continue; // skip header
57 if (line.isBlank()) continue;
58
59 try {
60 students.add(parseLine(line, lineNum));
61 } catch (ParseException e) {
62 // Collect parse errors but continue processing
63 errors.add(String.format("Line %d: %s (got: '%s')",
64 e.getLineNumber(), e.getMessage(), e.getLine()));
65 }
66 }
67 } catch (FileNotFoundException e) {
68 throw new FileProcessingException(
69 "CSV file not found: " + csvPath, csvPath, "open", e);
70 } catch (IOException e) {
71 throw new FileProcessingException(
72 "Failed to read: " + csvPath, csvPath, "read", e);
73 }
74
75 if (!errors.isEmpty()) {
76 System.err.println("Parse warnings for " + csvPath + ":");
77 errors.forEach(e -> System.err.println(" " + e));
78 }
79
80 return students;
81 }
82
83 private static Student parseLine(String line, int lineNum) throws ParseException {
84 String[] parts = line.split(",");
85 if (parts.length != 3) {
86 throw new ParseException("Expected 3 fields, got " + parts.length, lineNum, line);
87 }
88
89 String name;
90 int age;
91 double gpa;
92
93 try {
94 name = parts[0].trim();
95 age = Integer.parseInt(parts[1].trim());
96 gpa = Double.parseDouble(parts[2].trim());
97 } catch (NumberFormatException e) {
98 throw new ParseException("Invalid number format", lineNum, line);
99 }
100
101 if (name.isBlank()) throw new ParseException("Name cannot be blank", lineNum, line);
102 if (age < 16 || age > 100) throw new ParseException("Age out of range: " + age, lineNum, line);
103 if (gpa < 0.0 || gpa > 4.0) throw new ParseException("GPA out of range: " + gpa, lineNum, line);
104
105 return new Student(name, age, gpa);
106 }
107
108 /**
109 * Writes students to a file, demonstrating try-with-resources with multiple resources.
110 */
111 public static void writeReport(List<Student> students, Path outputPath) {
112 try (BufferedWriter writer = Files.newBufferedWriter(outputPath)) {
113 writer.write("Name,Age,GPA,Grade\n");
114 for (Student s : students) {
115 String grade = switch ((int)(s.gpa() * 10)) {
116 case 40, 39, 38, 37 -> "A";
117 case 36, 35, 34, 33 -> "B";
118 case 32, 31, 30 -> "C";
119 default -> "F";
120 };
121 writer.write(String.format("%s,%d,%.2f,%s%n",
122 s.name(), s.age(), s.gpa(), grade));
123 }
124 } catch (IOException e) {
125 throw new FileProcessingException(
126 "Failed to write report", outputPath, "write", e);
127 }
128 }
129
130 public static void main(String[] args) {
131 // Demonstrate with in-memory path (simulated)
132 System.out.println("File processing demo:");
133 System.out.println("- try-with-resources ensures files are closed");
134 System.out.println("- Custom exceptions carry context (file, operation)");
135 System.out.println("- Exception chaining preserves original IOException");
136 System.out.println("- Parse errors are collected, not fail-fast");
137
138 // Test the parseLine method directly
139 try {
140 Student s = parseLine("Alice,22,3.85", 1);
141 System.out.println("Parsed: " + s);
142
143 parseLine("Bob,abc,3.5", 2); // triggers NumberFormatException
144 } catch (ParseException e) {
145 System.out.println("Parse error at line " + e.getLineNumber()
146 + ": " + e.getMessage());
147 }
148 }
149}This program shows production-quality exception handling patterns: try-with-resources ensures BufferedReader is closed even if an exception is thrown, ParseException (checked) is caught per-line so one bad line doesn't abort the entire file, FileProcessingException (unchecked) wraps IOException with the file path and operation for richer error context, exception chaining preserves the original cause so the stack trace shows both the wrapper and the underlying IOException, and the multi-catch could be used to handle FileNotFoundException and IOException identically. The collect-and-continue pattern for parse errors is common in data import pipelines where maximum throughput is more important than fail-fast.
The next lesson covers File I/O in depth: reading and writing files with the NIO.2 API (java.nio.file.Files and Path), processing text files line by line, reading and writing binary data, working with directories, and watching for file system changes. These skills are needed in virtually every backend application that handles data import/export, logging, or configuration.
Your progress is saved in this browser, on this device only.