Exception Handling
checked vs unchecked, try-with-resources, exception chaining, custom exceptions
When to Use Exceptions
Exceptions express "unexpected but needs handling" situations: file read failure, network disconnect, DB connection timeout, invalid user input, JSON parse error. Java designs them as throwable/catchable objects rather than return values, so the caller can handle them in place, propagate them up, or catch them all at a top level.
- ✅ Good cases: IO failure / network error / resource unavailable / business validation failure / missing config
- ❌ Bad cases: using exceptions for control flow (e.g. using NumberFormatException to test whether a string is a number)
- Rule: if "errors" are a normal part of the flow (like form validation), consider a return value / Optional; only throw for true "exceptions"
throw and throws
throw raises an exception object inside a method; throws appears in the method signature, declaring which checked exceptions the method may throw and forcing the caller to either catch or keep declaring throws.
// ThrowDemo.java
public class ThrowDemo {
static int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("divide by zero");
}
return a / b;
}
public static void main(String[] args) {
System.out.println(divide(10, 2));
try {
System.out.println(divide(10, 0));
} catch (ArithmeticException e) {
System.out.println("caught: " + e.getMessage());
}
}
}checked vs unchecked
Java splits exceptions in two: checked must be handled or declared with throws, enforced by the compiler; unchecked (RuntimeException and subclasses, Error) are not enforced.
- checked: IOException, SQLException, ClassNotFoundException, etc. — common in library APIs, forcing callers to face them
- unchecked (the RuntimeException family): NullPointerException, IllegalArgumentException, IllegalStateException, IndexOutOfBoundsException — mostly "programmer errors"
- Error: OutOfMemoryError, StackOverflowError — JVM-level failures, don't catch them
// CheckedVsUnchecked.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CheckedVsUnchecked {
// must throws or handle in place (checked)
static String readConfig(String path) throws IOException {
return Files.readString(Path.of(path));
}
// no declaration needed (unchecked)
static int parseAge(String s) {
int n = Integer.parseInt(s); // parse failure throws NumberFormatException
if (n < 0) throw new IllegalArgumentException("age < 0");
return n;
}
public static void main(String[] args) throws IOException {
System.out.println(parseAge("30"));
try {
parseAge("abc");
} catch (NumberFormatException e) {
System.out.println("bad number: " + e.getMessage());
}
}
}try-catch-finally and Multi-catch
A finally block runs whether or not an exception is thrown, commonly used for resource cleanup (the old way). Multi-catch combines with `|`: handle several exceptions the same way.
// TryCatch.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
public class TryCatch {
public static void main(String[] args) {
try {
String content = Files.readString(Path.of("/no/such/file"));
System.out.println(content);
} catch (NoSuchFileException e) {
System.out.println("file missing: " + e.getFile());
} catch (IOException | RuntimeException e) {
// handle several exceptions together
System.out.println("other error: " + e);
} finally {
System.out.println("cleanup runs anyway");
}
}
}try-with-resources (Essential for IO / DB)
Anything "must be closed once opened" — file streams, DB connections, HTTP clients, sockets — should use try-with-resources. An object declared in the parentheses, as long as it implements AutoCloseable, is close()d automatically when leaving the try block — shorter and safer than try-finally (avoids forgetting to close, avoids close() throwing and swallowing the original exception).
// TryWithResources.java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class TryWithResources {
public static void main(String[] args) throws IOException {
Path p = Files.createTempFile("demo", ".txt");
Files.writeString(p, "line1\nline2\nline3");
// multiple resources are separated by semicolons; closed in reverse declaration order
try (BufferedReader r = Files.newBufferedReader(p)) {
String line;
while ((line = r.readLine()) != null) {
System.out.println(line);
}
}
// by here, r has already been closed automatically
Files.delete(p);
}
}Exception Chaining: Wrap, Don't Swallow
When a low-level exception is unfriendly to the upper layer (e.g. exposing SQLException to a Controller is inappropriate), wrap it to keep the low-level exception as the cause. Then throw new XxxException("business note", e) — both the call stack and the original exception show up in logs.
// ExceptionChain.java
public class ExceptionChain {
static String loadUser(long id) throws UserNotFoundException {
try {
// pretend this is a SQL query
if (id <= 0) throw new RuntimeException("SQL error: bad id");
return "Alice";
} catch (RuntimeException dbErr) {
throw new UserNotFoundException("load user " + id + " failed", dbErr);
}
}
public static void main(String[] args) {
try {
loadUser(-1);
} catch (UserNotFoundException e) {
System.out.println(e.getMessage());
System.out.println("caused by: " + e.getCause());
// e.printStackTrace(); // full chain
}
}
}
class UserNotFoundException extends Exception {
public UserNotFoundException(String msg, Throwable cause) {
super(msg, cause);
}
}Custom Exceptions
The business layer often needs its own exception types so callers can catch by type. Convention: business exceptions usually extend RuntimeException (unchecked, to avoid polluting signatures with throws everywhere), named ending in Exception.
// CustomException.java
public class CustomException {
public static void main(String[] args) {
try {
validate("");
} catch (ValidationException e) {
System.out.println(e.getField() + ": " + e.getMessage());
}
}
static void validate(String name) {
if (name == null || name.isBlank()) {
throw new ValidationException("name", "must not be blank");
}
}
}
class ValidationException extends RuntimeException {
private final String field;
public ValidationException(String field, String message) {
super(message);
this.field = field;
}
public String getField() { return field; }
}Practical Points
- Never write an empty catch block; at least log it
- Don't catch Exception, do nothing, then throw a new exception with no cause — that loses the stack trace
- Throw RuntimeException for business-flow failures; use checked only when a library must force the caller to handle it
- When a low-level exception is meaningless to the upper layer, wrap it: throw new XxxException("msg", originalException)
- Always replace hand-written finally close with try-with-resources
- Frameworks like Spring almost always convert checked to unchecked — the mainstream trend is to use checked sparingly