Exception handling
All errors extend Throwable:
Error: serious JVM problems such asOutOfMemoryError. Don't catch these.- Checked exceptions (
IOException,SQLException): the compiler forces you to catch or declare them. - Unchecked exceptions (
RuntimeExceptionand subclasses such asNullPointerExceptionandIllegalArgumentException): usually programming bugs.
try-with-resources (Java 7) closes anything that implements AutoCloseable, even when an exception is thrown. Multi-catch (catch (A | B e)) handles several exception types in one block.
Since Java 14, helpful NullPointerExceptions tell you exactly which variable or call was null.
The exception hierarchy
Everything thrown extends Throwable:
Error: serious JVM problems such asOutOfMemoryErrorandStackOverflowError. Don't catch these.Exception: problems a program can handle.RuntimeException(a subclass ofException): programming errors such asNullPointerException,IllegalArgumentExceptionandIndexOutOfBoundsException.
Checked vs unchecked exceptions
Checked exceptions (IOException, SQLException) must be caught or declared with throws; the compiler enforces it. Unchecked exceptions (RuntimeException and its subclasses) don't have to be. Use checked exceptions for recoverable situations the caller must think about; use unchecked for bugs and invalid arguments. Modern frameworks such as Spring mostly use unchecked exceptions.
try, catch and finally
Code that may fail goes in try. Each catch handles one kind of exception; put the most specific first. finally runs whether or not an exception happened (unless the JVM exits), which makes it the classic place for clean-up.
try {
int n = Integer.parseInt(input);
System.out.println(100 / n);
} catch (NumberFormatException e) {
System.out.println("Please enter a number");
} catch (ArithmeticException e) {
System.out.println("Can't divide by zero");
} finally {
System.out.println("Done");
}Multi-catch
Since Java 7 one catch can handle several unrelated exception types with the same code. The types can't be subclasses of each other.
try {
loadConfig();
} catch (IOException | IllegalStateException e) {
log.error("Couldn't load config", e);
}try-with-resources
Anything that implements AutoCloseable (files, streams, connections) can be declared in the try header; Java closes it automatically, in reverse order, even if an exception is thrown. If closing also fails, that exception is attached as suppressed instead of hiding the original.
try (var in = Files.newBufferedReader(path);
var out = Files.newBufferedWriter(copy)) {
in.transferTo(out);
} // both closed here, out first, then in
// Java 9+: an existing effectively final resource
BufferedReader reader = Files.newBufferedReader(path);
try (reader) { System.out.println(reader.readLine()); }throw and throws
throw raises an exception object right now. throws in a method signature declares checked exceptions the method may pass on to its caller.
public Course find(String slug) throws CourseNotFoundException {
return repo.findBySlug(slug)
.orElseThrow(() -> new CourseNotFoundException(slug));
}
void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("age can't be negative: " + age);
}Custom exceptions
Create your own exception when callers need to handle a specific situation, or when you want a clear name in logs. Extend RuntimeException (unchecked) or Exception (checked), pass a helpful message, and add fields for context.
public class InsufficientBalanceException extends RuntimeException {
private final long shortByPaise;
public InsufficientBalanceException(long shortByPaise) {
super("Balance is short by " + shortByPaise / 100.0 + " rupees");
this.shortByPaise = shortByPaise;
}
public long shortByPaise() { return shortByPaise; }
}Exception chaining (keeping the cause)
When you translate a low-level exception into a higher-level one, pass the original as the cause. The stack trace then shows both, and nothing is lost for debugging.
try {
return jdbc.query(sql);
} catch (SQLException e) {
throw new DataAccessException("Couldn't load orders for user " + userId, e); // e is the cause
}Best practices
- Never swallow exceptions with an empty
catch; at least log them. - Catch the most specific type you can handle; avoid
catch (Exception e)except at the top level. - Don't use exceptions for normal control flow; they're slow and hide intent.
- Validate early:
Objects.requireNonNull, argument checks at the start of methods. - Log an exception once, where it's handled, not at every layer.
- Include context in messages (ids, values), but never secrets.
finally pitfalls
A return inside finally silently replaces any exception or earlier return value, and an exception thrown in finally hides the original one. Keep finally for clean-up only, or better, use try-with-resources.
static int broken() {
try {
throw new IllegalStateException("real problem");
} finally {
return 0; // the exception disappears: never do this
}
}Checked exceptions in lambdas and streams
Standard functional interfaces don't allow checked exceptions, so a lambda that calls, say, Files.readString won't compile as is. Handle the exception inside the lambda, or wrap it in an unchecked exception such as UncheckedIOException.
List<String> contents = paths.stream()
.map(p -> {
try {
return Files.readString(p);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
})
.toList();Handling exceptions globally in Spring Boot
In a REST API, don't wrap every controller method in try-catch. A @RestControllerAdvice class turns exceptions into consistent JSON error responses (ProblemDetail, RFC 9457) in one place. The REST API lesson shows the full setup.
@RestControllerAdvice
class ApiErrors {
@ExceptionHandler(CourseNotFoundException.class)
ProblemDetail notFound(CourseNotFoundException e) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
}
}Old way vs new way
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
return reader.readLine();
} finally {
if (reader != null) {
try { reader.close(); } catch (IOException ignored) { }
}
}public String firstLine(Path file) {
try (BufferedReader reader = Files.newBufferedReader(file)) { // closed automatically
return reader.readLine();
} catch (NoSuchFileException | AccessDeniedException e) {
throw new CourseFileException("Cannot open " + file, e); // keep the cause
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
class CourseFileException extends RuntimeException {
CourseFileException(String msg, Throwable cause) { super(msg, cause); }
}Common mistake
Writing catch (Exception e) { } and silently swallowing the error. At minimum log it; usually rethrow it wrapped, with the original as the cause.
Under the hood
If both the try block and close() throw, the close exception is attached as a suppressed exception (getSuppressed()) instead of hiding the original. Creating exceptions is relatively expensive because of the stack trace, so use them for exceptional cases, not normal control flow. In Spring, @Transactional rolls back on unchecked exceptions by default but not on checked ones unless you set rollbackFor.
Check yourself
Which interface must a resource implement to be used in try-with-resources?
How this connects
Know these first
Part of Java from zero, Job-ready backend developer, Crack the Java interview.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.