Java · lesson 27 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Exception Handling in Java
Learn how Java handles errors — the exception hierarchy, try/catch/finally, checked vs unchecked exceptions, throw/throws, and creating custom exceptions.
By SkillByExample · Updated
An exception is an object that interrupts normal flow and travels up the call stack until a
catch handles it — or until it reaches the top and the program exits with a stack trace. This
lesson covers the handling side: try/catch/finally, throw and throws, resources,
custom exceptions and chaining. (For what each kind of error is, see
Error Types in Java.) Every example is one file run with the JDK 21 source
launcher (java File.java).
try / catch / finally: the order things run
public class Flow {
public static void main(String[] args) {
run("10");
run("ten");
}
static void run(String input) {
try {
System.out.println("try: parsing " + input);
int n = Integer.parseInt(input);
System.out.println("try: parsed " + n);
} catch (NumberFormatException e) {
System.out.println("catch: " + e.getMessage());
} finally {
System.out.println("finally");
}
System.out.println("after the try statement");
}
}
$ java Flow.java
try: parsing 10
try: parsed 10
finally
after the try statement
try: parsing ten
catch: For input string: "ten"
finally
after the try statement
When parseInt throws, the rest of the try block (“parsed”) is skipped and control jumps to the
matching catch. finally runs on both paths, and because the exception was handled, execution
continues normally afterwards.
Several catch blocks: most specific first
The first catch whose type matches wins. Putting a superclass before its subclass makes the
subclass block unreachable, and the compiler rejects it.
public class CatchOrder {
public static void main(String[] args) {
try {
Integer.parseInt("x");
} catch (IllegalArgumentException e) {
System.out.println("bad argument");
} catch (NumberFormatException e) {
System.out.println("bad number");
}
}
}
$ java CatchOrder.java
CatchOrder.java:7: error: exception NumberFormatException has already been caught
} catch (NumberFormatException e) {
^
1 error
error: compilation failed
NumberFormatException extends IllegalArgumentException, so the first block already catches
it. Swap the two blocks and the program prints bad number. When two unrelated types need the
same handling, combine them with multi-catch: catch (IOException | SQLException e).
finally overrides return
finally runs even after a return in try — and if finally itself returns, it replaces the
original result and discards any exception.
public class FinallyReturn {
public static void main(String[] args) {
System.out.println(normal());
System.out.println(overridden());
System.out.println(swallowed());
}
static String normal() {
try { return "from try"; }
finally { System.out.println("finally ran"); }
}
@SuppressWarnings("finally")
static String overridden() {
try { return "from try"; }
finally { return "from finally"; }
}
@SuppressWarnings("finally")
static String swallowed() {
try { throw new IllegalStateException("lost"); }
finally { return "no exception?"; }
}
}
$ java FinallyReturn.java
finally ran
from try
from finally
no exception?
The IllegalStateException in swallowed() vanished without a trace. Use finally for cleanup
only, never for return or throw; javac flags it with -Xlint:finally, which the annotations
here silence for the demonstration.
throw, throws and custom exceptions
A custom exception carries domain data the caller can act on. Extending Exception makes it
checked, so throws must declare it and every caller must handle it.
public class Bank {
public static void main(String[] args) {
Account acc = new Account(100);
try {
acc.withdraw(30);
acc.withdraw(150);
System.out.println("not reached");
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
System.out.printf("shortfall: %.2f%n", e.shortfall());
}
}
}
class InsufficientFundsException extends Exception {
private final double requested, available;
InsufficientFundsException(double requested, double available) {
super(String.format("cannot withdraw %.2f, balance is %.2f", requested, available));
this.requested = requested;
this.available = available;
}
double shortfall() { return requested - available; }
}
class Account {
private double balance;
Account(double balance) { this.balance = balance; }
void withdraw(double amt) throws InsufficientFundsException {
if (amt <= 0) throw new IllegalArgumentException("amount must be positive: " + amt);
if (amt > balance) throw new InsufficientFundsException(amt, balance);
balance -= amt;
}
}
$ java Bank.java
cannot withdraw 150.00, balance is 70.00
shortfall: 80.00
throw raises an exception; throws in the signature declares one may escape. The negative-amount
check uses the unchecked IllegalArgumentException — that’s a caller bug, not a situation to
recover from, so it needs no declaration.
try-with-resources
Any AutoCloseable declared in the try (...) header is closed automatically, in reverse order,
whether the block finishes or throws.
public class Resources {
public static void main(String[] args) {
try (Res a = new Res("db"); Res b = new Res("file")) {
System.out.println("working with " + a.name + " and " + b.name);
throw new IllegalStateException("work failed");
} catch (IllegalStateException e) {
System.out.println("caught: " + e.getMessage());
}
}
}
class Res implements AutoCloseable {
final String name;
Res(String name) { this.name = name; System.out.println("open " + name); }
public void close() { System.out.println("close " + name); }
}
$ java Resources.java
open db
open file
working with db and file
close file
close db
caught: work failed
Both resources were closed — file first, since it was opened last — before the catch ran.
The equivalent hand-written finally needs null checks and nested trys to be equally safe.
Suppressed exceptions
If the body throws and close() throws too, the body’s exception wins and the close failure is
attached to it rather than lost.
public class Suppressed {
public static void main(String[] args) {
try (Flaky f = new Flaky()) {
throw new IllegalStateException("body failed");
} catch (IllegalStateException e) {
System.out.println("main: " + e.getMessage());
for (Throwable s : e.getSuppressed()) System.out.println("suppressed: " + s.getMessage());
}
}
}
class Flaky implements AutoCloseable {
public void close() { throw new IllegalStateException("close failed"); }
}
$ java Suppressed.java
main: body failed
suppressed: close failed
With a plain finally { f.close(); }, the close failure would have replaced “body failed” —
hiding the original problem.
Chaining: wrap but keep the cause
When translating a low-level exception into a domain one, pass the original as the cause.
public class Chaining {
public static void main(String[] args) {
try {
loadPort("port=eighty");
} catch (ConfigException e) {
System.out.println(e.getMessage());
System.out.println("cause: " + e.getCause());
}
}
static int loadPort(String line) throws ConfigException {
try {
return Integer.parseInt(line.substring(line.indexOf('=') + 1));
} catch (NumberFormatException e) {
throw new ConfigException("invalid port in '" + line + "'", e);
}
}
}
class ConfigException extends Exception {
ConfigException(String message, Throwable cause) { super(message, cause); }
}
$ java Chaining.java
invalid port in 'port=eighty'
cause: java.lang.NumberFormatException: For input string: "eighty"
Dropping e from the constructor call compiles fine but throws away the only clue to what
actually went wrong; an uncaught ConfigException would then print no Caused by: section.
Best practices
- Catch the most specific type you can actually handle; let the rest propagate.
- Never leave a
catchblock empty — handle, log, or rethrow. - Use checked exceptions for conditions callers can recover from, unchecked for bugs.
- Close resources with try-with-resources, not
finally. - Wrap with a cause; don’t replace one exception with another and lose the first.
- Don’t use exceptions for ordinary control flow — check the condition instead where you can.
Practice
- Write
safeDivide(String a, String b)that returns the quotient, and handles bad numbers and division by zero with different messages. Test"10","2","10","x"and"10","0".
Solution
public class SafeDivide {
public static void main(String[] args) {
System.out.println(safeDivide("10", "2"));
System.out.println(safeDivide("10", "x"));
System.out.println(safeDivide("10", "0"));
}
static String safeDivide(String a, String b) {
try {
return "result " + Integer.parseInt(a) / Integer.parseInt(b);
} catch (NumberFormatException e) {
return "not a number: " + e.getMessage();
} catch (ArithmeticException e) {
return "cannot divide by zero";
}
}
}
$ java SafeDivide.java
result 5
not a number: For input string: "x"
cannot divide by zero
The two catch blocks are unrelated types, so their order doesn’t matter here.
parseAgebelow swallows the exception:try { return Integer.parseInt(s); } catch (NumberFormatException e) { } return 0;Change it so the failure is reported and the caller still learns something went wrong.
Solution
public class NoSwallow {
public static void main(String[] args) {
try {
System.out.println(parseAge("forty"));
} catch (IllegalArgumentException e) {
System.out.println("caller sees: " + e.getMessage() + " (cause: " + e.getCause().getClass().getSimpleName() + ")");
}
}
// was: try { return Integer.parseInt(s); } catch (NumberFormatException e) { } return 0;
static int parseAge(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("age must be a whole number: " + s, e);
}
}
}
$ java NoSwallow.java
caller sees: age must be a whole number: forty (cause: NumberFormatException)
Returning 0 would have produced a person aged zero with no clue why. Rethrowing with context keeps the failure visible and the cause attached.
- Use try-with-resources with three
Resobjects and an exception thrown in the body. Predict the close order and whethercatchorclosehappens first.
Solution
public class ThreeResources {
public static void main(String[] args) {
try (Res a = new Res("A"); Res b = new Res("B"); Res c = new Res("C")) {
throw new RuntimeException("boom");
} catch (RuntimeException e) {
System.out.println("catch " + e.getMessage());
} finally {
System.out.println("finally");
}
}
}
class Res implements AutoCloseable {
private final String name;
Res(String name) { this.name = name; }
public void close() { System.out.println("close " + name); }
}
$ java ThreeResources.java
close C
close B
close A
catch boom
finally
Resources close in reverse order of opening, all before catch; finally comes last.
- Make
InsufficientFundsExceptionunchecked by extendingRuntimeException. What changes inAccount.withdraw, and what can callers now forget?
Solution
public class Unchecked {
public static void main(String[] args) {
Account acc = new Account(100);
acc.withdraw(150); // compiles with no try/catch
System.out.println("not reached");
}
}
class InsufficientFundsException extends RuntimeException {
InsufficientFundsException(String msg) { super(msg); }
}
class Account {
private double balance;
Account(double b) { balance = b; }
void withdraw(double amt) { // no throws clause needed
if (amt > balance) throw new InsufficientFundsException("balance is " + balance);
balance -= amt;
}
}
$ java Unchecked.java
Exception in thread "main" InsufficientFundsException: balance is 100.0
at Account.withdraw(Unchecked.java:17)
at Unchecked.main(Unchecked.java:4)
The throws clause disappears and the compiler no longer reminds callers to handle it, so this
one crashed the program. That is the trade-off: checked for expected business failures, unchecked
for bugs.