The compiler that wouldn't let go
Riya's fee-reminder program reads due dates like “15-07-2025” with SimpleDateFormat. It won't compile: unreported exception ParseException; must be caught or declared to be thrown. Yet last week her Integer.parseInt code compiled without any complaint, even though it could also fail on bad text.
Sir, both methods can fail on bad text. Why does the compiler force me to handle only one of them?
Because the people who designed those methods made different choices. parse declares a checked exception: “this can fail for reasons you must plan for.” parseInt throws an unchecked one: “if you pass bad input, that's a bug or bad data; handle it if you want.”
So checked ones happen at compile time?
No! That's the classic mistake. All exceptions happen at run time. “Checked” only means the compiler checks that you've planned for it, by catching it or declaring it with throws.
What are checked and unchecked exceptions?
Checked exceptions are Throwable classes that are not subclasses of RuntimeException or Error (for example IOException, ParseException, ClassNotFoundException). For these, the compiler enforces the catch-or-declare rule: any code that can throw one must either catch it or declare it in the method's throws clause, otherwise compilation fails. Unchecked exceptions are RuntimeException, Error and their subclasses; the compiler does not require them to be caught or declared.
Checked = “this might go wrong even if your code is perfect (missing file, network down), so show me your plan.” Unchecked = “this usually means a bug or bad input; I won't force you, but fix the cause.”
Words you'll meet in this lesson
void read() throws IOException: “callers, this may reach you.”getCause().Checked vs unchecked at a glance
| Checked | Unchecked | |
|---|---|---|
| Class hierarchy | Exception, but not RuntimeException | RuntimeException or Error (and subclasses) |
| Compiler forces catch or throws? | Yes: unreported exception … | No |
| When does it happen? | At run time | At run time |
| Typical meaning | Outside problem you should plan for | Bug, bad argument or bad data |
| Examples | IOException, FileNotFoundException, ParseException, SQLException, ClassNotFoundException, InterruptedException | NullPointerException, ArithmeticException, IllegalArgumentException, NumberFormatException, IndexOutOfBoundsException |
| Usual handling | Catch where you can recover, or declare and let a caller decide | Prevent with checks; catch at a boundary (request, record, job) |
Exam tip: “Checked exceptions are checked at compile time” means the handling is checked at compile time. The exception itself is still thrown at run time. Also: Error is unchecked even though it is not a RuntimeException.
Same job, two different designs
Checked: outside world
Files, networks, databases, dates typed by users with the old API. The failure isn't your bug, so you must have a plan.
IOException, SQLExceptionUnchecked: bugs and bad arguments
Null references, wrong indexes, invalid values passed to a method. Prevent them with checks rather than catch blocks everywhere.
RuntimeExceptionWrapped: checked inside unchecked
Catch a checked exception and rethrow it as an unchecked one with the original as its cause. Common in modern frameworks and lambdas.
new X(msg, cause)Parsing a date: old API (checked) vs new API (unchecked)
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
public class Main {
public static void main(String[] args) {
// Old API: parse() declares the checked ParseException
try {
new SimpleDateFormat("yyyy-MM-dd").parse("2025/01/26");
} catch (ParseException e) {
System.out.println("Checked: " + e);
}
// New API: LocalDate.parse() throws the unchecked DateTimeParseException
try {
LocalDate.parse("2025/01/26");
} catch (DateTimeParseException e) {
System.out.println("Unchecked: " + e);
}
}
}Delete the first try/catch and the program no longer compiles. Delete the second and it compiles fine, but would crash at run time on bad input. java.time arrived in Java 8 and, like most modern APIs, chose unchecked exceptions.
When should you use which?
| Situation | Choose | Example |
|---|---|---|
| Caller passed an invalid value | Unchecked | IllegalArgumentException("Invalid GST slab: 15%") |
| Object is in the wrong state for this call | Unchecked | IllegalStateException("Order already shipped") |
| A failure the caller can reasonably recover from and must think about | Checked | Insufficient funds in a banking API (Lesson 9.5) |
| You can't do anything useful with a library's checked exception here | Wrap in unchecked, keep the cause | new UncheckedIOException("Could not load fees", e) |
Lambdas and checked exceptions don't mix: names.forEach(n -> Thread.sleep(10)); fails with unreported exception InterruptedException, because the functional interfaces used by forEach and streams don't declare any checked exceptions. Catch inside the lambda or wrap the exception.
How javac applies the catch-or-declare rule
javac reads the called method's signature
Both methods list an exception afterpublic static void sleep(long millis) throws InterruptedException public static int parseInt(String s) throws NumberFormatExceptionthrows. javac looks at each one's class:InterruptedExceptionextendsExceptiondirectly, so it is checked.NumberFormatExceptionis a RuntimeException, so javac ignores it for the rule.No catch and no throws? Compile error
This check happens only in the compiler. No class file is produced, so the program never runs.Main.java:4: error: unreported exception InterruptedException; must be caught or declared to be thrown Thread.sleep(500); ^Option A: catch it where you can deal with it
The call is inside a try with a catch for that type (or a parent such astry { Thread.sleep(500); } catch (InterruptedException e) { System.out.println("Reminder delay interrupted"); }Exception), so the rule is satisfied.Option B: declare it and let the caller decide
Now every caller ofstatic void sendReminder(String student) throws InterruptedException { Thread.sleep(500); }sendReminderfaces the same rule. The obligation moves up one level, until some method catches it. (More onthrowsin Lesson 9.4.)Option C: wrap it in an unchecked exception with its cause
The new object stores the original in itscatch (ParseException e) { throw new IllegalArgumentException("Bad due date " + text, e); }causefield. A stack trace then shows both parts:java.lang.IllegalArgumentException: Bad due date 31-06-2025 at Main.saveDueDate(Main.java:11) at Main.main(Main.java:17) Caused by: java.text.ParseException: Unparseable date: "31-06-2025" at java.base/java.text.DateFormat.parse(DateFormat.java:399) at Main.saveDueDate(Main.java:9) ... 1 more
Compile time vs run time: both views
Left: the line and what javac or the JVM says. Right: what javac checks before the program exists, and what the JVM does once it runs.
- Compile error:
Thread.sleepdeclares the checkedInterruptedException, and this code neither catches nor declares it. - Fixed: with a matching catch, javac compiles the code. Nothing has run yet; we only proved there's a plan.
- No complaint: integer division can throw
ArithmeticException, but it is unchecked, so javac says nothing. - Run time: with
count = 0the program crashes. The compiler never warned us: preventing this was our job. - Wrap: a
ParseExceptionwas caught and rethrown as an uncheckedIllegalArgumentExceptionwith a clearer message. - Cause kept:
getCause()returns the original ParseException, so no detail is lost.
- javac finds the method being called and reads its
throwsclause. InterruptedException is not under RuntimeException or Error, so it is checked, and there is no catch or throws: error. - The same check again, but now the call sits inside a try whose catch covers
InterruptedException. The class file is written. - For the division, the only possible exception is unchecked, so javac has nothing to enforce.
- At run time the JVM throws and searches for handlers exactly as in Lesson 9.1. The JVM does not know or care whether an exception is checked: that idea belongs only to the Java compiler.
- The wrapper object #2 is created with
(message, cause). Itscausefield holds a reference to the original #1. getCause()simply returns that reference. Stack traces print it in the “Caused by:” section.
Checked and unchecked in real systems
Big codebases mix both styles. Knowing which one an API uses tells you what you must handle.
Watch it with an example: an exception travelling through three layers
A web app's database layer gets a checked SQLException. Each layer decides what to do: wrap it, pass it on, or turn it into an HTTP response.
- The failure: FeeDao's JDBC call fails because the database is down: a checked
SQLException. - Wrap once: FeeDao can't fix a dead database, so it catches it and throws an unchecked
FeeLoadException("…", e)with #1 as the cause. - Pass through: FeeService has nothing useful to do with it. Since it's unchecked, the service needs no try/catch and no
throwsclause. - Boundary: the controller (or a global handler) catches it and sends a clean HTTP 500 response. The user sees a friendly message, not a stack trace.
- Nothing lost: the log keeps the full chain: the wrapper plus Caused by: SQLException, so engineers can find the real reason.
- The pattern: catch checked exceptions where they occur, wrap them with the cause, handle them once at the edge. Spring does exactly this with its
DataAccessExceptionfamily. (Simplified.)
1. Reading files and configuration
File APIs in java.io and java.nio.file throw the checked IOException. Start-up code reading application.properties or a CSV upload must decide: retry, use defaults, or stop with a clear error.
2. Databases: SQLException and friends
Plain JDBC throws the checked SQLException from almost every call. Spring translates it into its unchecked DataAccessException family, so service code isn't full of try/catch blocks.
3. Streams and lambdas
Lambdas can't throw checked exceptions through map or forEach. The JDK itself wraps: Files.lines(path) reports read failures during the stream as the unchecked UncheckedIOException.
4. Validating API input
Service methods reject bad arguments with unchecked exceptions like IllegalArgumentException. A global handler in the web layer turns them into an HTTP 400 response with the message.
Examples are simplified. Teams usually agree on a convention, often “unchecked for business and validation errors, wrap library checked exceptions at the boundary”, and apply it consistently.
Riya's due-date converter
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class Main {
static String toIso(String text) throws ParseException {
SimpleDateFormat in = new SimpleDateFormat("dd-MM-yyyy");
in.setLenient(false);
return new SimpleDateFormat("yyyy-MM-dd").format(in.parse(text));
}
public static void main(String[] args) {
String[] dueDates = {"15-07-2025", "31-06-2025", "15/08/2025"};
for (String d : dueDates) {
try {
System.out.println(d + " -> " + toIso(d));
} catch (ParseException e) {
System.out.println(d + " -> rejected: " + e.getMessage());
}
}
}
}throws ParseExceptionOption B:toIsodeclares the checked exception instead of catching it.in.setLenient(false)Without it, 31 June would quietly become 1 July.catch (ParseException e)main is where we know what to do: reject that date and move on.e.getMessage()Names the exact text that failed, useful for the user and for logs.
Wrapping a checked exception on its way up
A checked ParseException is thrown two calls deep, caught one level up and wrapped in an unchecked exception that keeps it as its cause.
Worked examples: real problems, solved and traced in memory
Three situations where the checked or unchecked choice changes the code you must write. Run each one, then step through it and watch exception objects (and their causes) appear on the heap while frames are popped.
A checked ParseException travels two frames
Problem statement
A booking form sends the journey date as text. parseJourney converts it with SimpleDateFormat, whose parse throws the checked ParseException. Neither helper can do anything useful about a bad date, so they declare it, and main handles it.
journey dates: "14-11-2025", then "31-11-2025" (November has 30 days)
Booked Kavya on 14-11-2025 Booking failed: Unparseable date: "31-11-2025"
Approach
parseJourneydeclaresthrows ParseExceptioninstead of catching.bookcalls it and does not catch either, so the compiler makes it declarethrows ParseExceptiontoo.maincatches it: the compile-time chain ends there. At run time, the second date fails and the exception skips both helper frames.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
An unchecked exception for a bad argument
Problem statement
A billing module computes GST. Only the slabs 0, 5, 12, 18 and 28 percent are valid. A wrong rate is a programming or data error, so gstAmount throws the unchecked IllegalArgumentException. Callers are not forced to catch it; here main catches it to print a clear message.
item 1: Rs 1000 at 18% item 2: Rs 500 at 15% (not a valid slab)
Headphones: Rs 1000 + GST Rs 180 Billing error: Invalid GST slab: 15%
Approach
gstAmountchecks the rate and throws if it isn't a valid slab. Nothrowsclause is needed for an unchecked exception.billLinecalls it without any try or throws, and the compiler is happy.- At run time the second line throws; the exception passes through
billLineand is caught inmain.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Wrap a checked exception, keep the cause
Problem statement
At start-up an app loads its payment plugin class by name. Class.forName throws the checked ClassNotFoundException. The start-up code wraps it in an unchecked IllegalStateException with a clear message, keeping the original as the cause.
plugin class name = "in.myshop.pay.UpiPlugin" (not on the classpath)
Start-up failed: Payment plugin missing Root cause: java.lang.ClassNotFoundException: in.myshop.pay.UpiPlugin
Approach
loadPlugincatches the checked exception right where it happens.- It throws a new
IllegalStateException(message, cause); the original object becomes its cause. maincatches the wrapper and usesgetCause()to reach the original problem.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 9.3 in one screen
- Checked = Exception but not RuntimeException. Unchecked = RuntimeException, Error and subclasses.
- Checked exceptions must be caught or declared (
throws), else: unreported exception … must be caught or declared to be thrown. - Every exception happens at run time; the check is a compile-time rule only.
- Catching a checked exception the try can't throw is a compile error;
catch (Exception e)is always allowed. - Use unchecked for bad arguments and bugs; checked for recoverable outside failures the caller must consider.
- Wrap with
new XException("message", e)to keep the cause; read it withgetCause(). - Lambdas in streams can't throw checked exceptions: catch or wrap inside.
Interview and viva questions
What is the difference between checked and unchecked exceptions?
Checked exceptions (subclasses of Exception that are not RuntimeException) must be caught or declared, and the compiler enforces this. Unchecked exceptions (RuntimeException, Error and their subclasses) have no such rule. Both are thrown at run time; the difference is only whether the compiler forces you to plan for them.
Why did Java's designers create checked exceptions?
To make failures that are part of a method's normal contract, such as a missing file or a dropped network connection, visible in its signature, so that callers cannot forget them. The trade-off is extra code, and the rule doesn't work well with lambdas, which is why many modern APIs prefer unchecked exceptions.
Is it good practice to convert checked exceptions to unchecked?
Often yes, at a boundary where the code cannot recover, for example inside a lambda or a layer that only passes the problem upwards. Always keep the original as the cause, and choose a meaningful wrapper (UncheckedIOException, IllegalStateException or your own type) with a clear message.
Can a method declare an unchecked exception in throws?
Yes. Integer.parseInt declares throws NumberFormatException. It documents the behaviour, but the compiler ignores unchecked types for the catch-or-declare rule, so callers aren't forced to catch it.
Does the JVM treat checked exceptions differently at run time?
No. The JVM throws and handles all Throwables the same way. Checked exceptions are a rule of the Java compiler only, which is why other JVM languages such as Kotlin have no checked exceptions at all.
Practice problems
Practise recognising checked and unchecked exceptions, satisfying the compiler, and wrapping one kind in the other without losing the cause. The last three are real business tasks.
Checked or unchecked?
Write isChecked(Throwable t) that returns true only for checked exceptions, and test it on five exception objects.
IOException, NumberFormatException, ParseException, OutOfMemoryError, ClassNotFoundException
IOException: checked NumberFormatException: unchecked ParseException: checked OutOfMemoryError: unchecked ClassNotFoundException: checked
Show solution and explanation
public class Main {
static boolean isChecked(Throwable t) {
return !(t instanceof RuntimeException) && !(t instanceof Error);
}
public static void main(String[] args) {
Throwable[] list = {
new java.io.IOException(),
new NumberFormatException(),
new java.text.ParseException("bad", 0),
new OutOfMemoryError(),
new ClassNotFoundException()
};
for (Throwable t : list) {
System.out.println(t.getClass().getSimpleName() + ": " + (isChecked(t) ? "checked" : "unchecked"));
}
}
}How it works: the rule is about the class hierarchy only. Anything under RuntimeException or Error is unchecked; every other Throwable is checked. ParseException and ClassNotFoundException extend Exception directly or through a non-runtime parent, so they are checked.
Handle a checked ParseException
Convert dates from dd-MM-yyyy to yyyy-MM-dd with SimpleDateFormat. Its parse method throws the checked ParseException, so the code must catch it.
"26-01-2025", "26/01/2025", "30-02-2025"
26-01-2025 -> 2025-01-26 26/01/2025 -> Unparseable date: "26/01/2025" 30-02-2025 -> Unparseable date: "30-02-2025"
Show solution and explanation
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class Main {
public static void main(String[] args) {
SimpleDateFormat in = new SimpleDateFormat("dd-MM-yyyy");
in.setLenient(false);
SimpleDateFormat out = new SimpleDateFormat("yyyy-MM-dd");
String[] dates = {"26-01-2025", "26/01/2025", "30-02-2025"};
for (String d : dates) {
try {
System.out.println(d + " -> " + out.format(in.parse(d)));
} catch (ParseException e) {
System.out.println(d + " -> " + e.getMessage());
}
}
}
}How it works: remove the try/catch and javac stops with unreported exception ParseException; must be caught or declared to be thrown. setLenient(false) makes 30 February an error; without it, the old API quietly rolls it over into March.
The unchecked way to parse dates
Do the same conversion with LocalDate.parse and a DateTimeFormatter. It throws the unchecked DateTimeParseException: the code compiles without a try, but catch it anyway to show a message.
"26-01-2025", "26/01/2025", "30-02-2025"
26-01-2025 -> 2025-01-26 26/01/2025 -> Text '26/01/2025' could not be parsed at index 2 30-02-2025 -> Text '30-02-2025' could not be parsed: Invalid date 'FEBRUARY 30'
Show solution and explanation
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
public class Main {
public static void main(String[] args) {
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd-MM-uuuu")
.withResolverStyle(ResolverStyle.STRICT);
String[] dates = {"26-01-2025", "26/01/2025", "30-02-2025"};
for (String d : dates) {
try {
System.out.println(d + " -> " + LocalDate.parse(d, f));
} catch (DateTimeParseException e) {
System.out.println(d + " -> " + e.getMessage());
}
}
}
}How it works: the newer java.time API (Java 8+) chose an unchecked exception, so the compiler does not force a try. That makes calling it inside lambdas and streams easy, but it is now your job to remember that bad input throws. uuuu with STRICT rejects 30 February.
Is the class on the classpath?
Use Class.forName, which throws the checked ClassNotFoundException, to report which of these classes are available.
"java.util.ArrayList", "com.example.PaymentPlugin", "java.time.LocalDate"
Found: ArrayList Missing: com.example.PaymentPlugin Found: LocalDate
Show solution and explanation
public class Main {
public static void main(String[] args) {
String[] names = {"java.util.ArrayList", "com.example.PaymentPlugin", "java.time.LocalDate"};
for (String n : names) {
try {
Class<?> c = Class.forName(n);
System.out.println("Found: " + c.getSimpleName());
} catch (ClassNotFoundException e) {
System.out.println("Missing: " + e.getMessage());
}
}
}
}How it works: loading a class by name can fail for reasons outside your code, such as a missing JAR file, so the API designers made it a checked exception. The message is the class name that could not be found.
Wrap it, keep the cause
Write parseOrFail(text) that converts a date and, on failure, throws an unchecked IllegalArgumentException that keeps the original ParseException as its cause. In main, print the message and the cause.
"2025-13-01" with pattern yyyy-MM-dd (month 13)
Message: Bad date: 2025-13-01 Cause: java.text.ParseException: Unparseable date: "2025-13-01"
Show solution and explanation
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
static Date parseOrFail(String text) {
SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd");
f.setLenient(false);
try {
return f.parse(text);
} catch (ParseException e) {
throw new IllegalArgumentException("Bad date: " + text, e);
}
}
public static void main(String[] args) {
try {
parseOrFail("2025-13-01");
} catch (IllegalArgumentException e) {
System.out.println("Message: " + e.getMessage());
System.out.println("Cause: " + e.getCause());
}
}
}How it works: the two-argument constructor (message, cause) links the new exception to the old one. parseOrFail no longer needs a throws clause because IllegalArgumentException is unchecked, yet nothing is lost: getCause() returns the original, and a printed stack trace would show it under “Caused by:”.
Validate a URL
new URI(text) throws the checked URISyntaxException for malformed addresses. Check three addresses and print the host, or the reason and position of the error.
"https://shop.example.in/orders", "https://shop.example.in/search?q=red shoes", "http://my shop.in"
OK, host shop.example.in Bad: Illegal character in query at index 36 Bad: Illegal character in authority at index 9
Show solution and explanation
import java.net.URI;
import java.net.URISyntaxException;
public class Main {
public static void main(String[] args) {
String[] urls = {"https://shop.example.in/orders",
"https://shop.example.in/search?q=red shoes",
"http://my shop.in"};
for (String u : urls) {
try {
URI uri = new URI(u);
System.out.println("OK, host " + uri.getHost());
} catch (URISyntaxException e) {
System.out.println("Bad: " + e.getReason() + " at index " + e.getIndex());
}
}
}
}How it works: a space is not allowed in a URI, so both failing addresses are rejected. The checked exception carries extra details, getReason() and getIndex(), which are handy for showing the user exactly where the problem is.
Declare it, handle it higher up
Instead of catching inside the helper, declare the checked exception with throws and catch it in main. Show that a method in between must also declare it.
class names "java.util.HashMap" and "in.bank.Ledger"
Plugin HashMap ready Cannot load in.bank.Ledger
Show solution and explanation
public class Main {
static String load(String name) throws ClassNotFoundException {
return Class.forName(name).getSimpleName();
}
static String loadPlugin(String name) throws ClassNotFoundException {
return "Plugin " + load(name);
}
public static void main(String[] args) {
String[] names = {"java.util.HashMap", "in.bank.Ledger"};
for (String n : names) {
try {
System.out.println(loadPlugin(n) + " ready");
} catch (ClassNotFoundException e) {
System.out.println("Cannot load " + e.getMessage());
}
}
}
}How it works: the compiler checks every method: load can throw, so it declares throws; loadPlugin calls load and does not catch, so it must declare it too. main catches it, which ends the chain. Delete either throws clause and the program no longer compiles. Lesson 9.4 covers throws in detail.
Catch Exception, but not IOException?
Predict which of these blocks compile. A catch for a checked exception that the try body can never throw is a compile error, but catch (Exception e) is always allowed.
a try body that only prints
Block A Block B A and B compile; C would not
Show solution and explanation
public class Main {
public static void main(String[] args) {
try {
System.out.println("Block A");
} catch (Exception e) {
System.out.println("never runs");
}
try {
System.out.println("Block B");
} catch (RuntimeException e) {
System.out.println("never runs");
}
// try { System.out.println("C"); } catch (java.io.IOException e) { }
// ^ compile error: exception IOException is never thrown in body of corresponding try statement
System.out.println("A and B compile; C would not");
}
}How it works: javac knows exactly which checked exceptions a try body can throw, so a catch for an impossible checked exception is flagged as unreachable. Exception and RuntimeException are different: unchecked exceptions can come from almost any line, so catching them is always allowed.
Bank statement date check
A bank statement upload has transaction dates as text in dd-MM-yyyy. Count the valid and invalid dates, list the invalid ones, and report the earliest valid date.
dates = {"03-04-2025", "31-04-2025", "01-04-2025", "1st April", "15-04-2025"}Invalid date: 31-04-2025 Invalid date: 1st April 3 valid, 2 invalid Statement starts: 01-04-2025
Show solution and explanation
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
public static void main(String[] args) {
String[] dates = {"03-04-2025", "31-04-2025", "01-04-2025", "1st April", "15-04-2025"};
SimpleDateFormat f = new SimpleDateFormat("dd-MM-yyyy");
f.setLenient(false);
Date earliest = null;
String earliestText = "";
int valid = 0;
for (String d : dates) {
try {
Date parsed = f.parse(d);
valid++;
if (earliest == null || parsed.before(earliest)) {
earliest = parsed;
earliestText = d;
}
} catch (ParseException e) {
System.out.println("Invalid date: " + d);
}
}
System.out.println(valid + " valid, " + (dates.length - valid) + " invalid");
System.out.println("Statement starts: " + earliestText);
}
}How it works:
- April has 30 days, so with
setLenient(false)the date 31-04-2025 is rejected instead of silently becoming 1 May. - The checked exception forces us to decide what to do with bad rows; here they are listed for the customer to correct.
- We compare
Dateobjects withbefore, and keep the original text for printing so the output does not depend on the computer's time zone.
Payment callback URL whitelist
A payment gateway only sends results to callback URLs that use https and whose host is pay.myshop.in. Validate each merchant setting and print ACCEPT or REJECT with a reason.
"https://pay.myshop.in/cb", "http://pay.myshop.in/cb", "https://evil.example.com/cb", "https://pay.myshop.in/c b"
https://pay.myshop.in/cb -> ACCEPT http://pay.myshop.in/cb -> REJECT (not https) https://evil.example.com/cb -> REJECT (host evil.example.com) https://pay.myshop.in/c b -> REJECT (Illegal character in path)
Show solution and explanation
import java.net.URI;
import java.net.URISyntaxException;
public class Main {
static String check(String url) {
try {
URI u = new URI(url);
if (!"https".equals(u.getScheme())) return "REJECT (not https)";
if (!"pay.myshop.in".equals(u.getHost())) return "REJECT (host " + u.getHost() + ")";
return "ACCEPT";
} catch (URISyntaxException e) {
return "REJECT (" + e.getReason() + ")";
}
}
public static void main(String[] args) {
String[] urls = {"https://pay.myshop.in/cb", "http://pay.myshop.in/cb",
"https://evil.example.com/cb", "https://pay.myshop.in/c b"};
for (String u : urls) {
System.out.println(u + " -> " + check(u));
}
}
}How it works:
- Malformed text throws the checked
URISyntaxException, whichcheckturns into a readable rejection reason. - Well-formed URLs are then checked against the rules. Writing
"https".equals(…)avoids a NullPointerException when a URL has no scheme. - The method never throws; callers get a simple answer for each URL.
Settings loader that fails clearly
An app's start-up settings include a launch date and a port. If any setting is broken, start-up must stop with one clear unchecked exception naming the setting, with the original problem attached as the cause.
launchDate = "2025-02-29" (not a leap year), port = "8080"
Start-up stopped: Setting launchDate is invalid Because of: java.text.ParseException: Unparseable date: "2025-02-29"
Show solution and explanation
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class Main {
static void validate(String launchDate, String port) {
SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd");
f.setLenient(false);
try {
f.parse(launchDate);
} catch (ParseException e) {
throw new IllegalStateException("Setting launchDate is invalid", e);
}
try {
Integer.parseInt(port);
} catch (NumberFormatException e) {
throw new IllegalStateException("Setting port is invalid", e);
}
System.out.println("Settings OK");
}
public static void main(String[] args) {
try {
validate("2025-02-29", "8080");
} catch (IllegalStateException e) {
System.out.println("Start-up stopped: " + e.getMessage());
System.out.println("Because of: " + e.getCause());
}
}
}How it works:
- Two very different low-level problems (a checked
ParseException, an uncheckedNumberFormatException) are turned into the same kind of high-level exception with a message that names the setting. - The original exception is kept as the cause, so the root reason, Unparseable date, is still visible.
- Failing fast at start-up is better than running with a wrong launch date.
Real-world scenario questions (asked in company interviews)
Checked vs unchecked is a design decision, and interviewers want to hear your reasons. Decide your answer first, then open the model answer.
“I added throws Exception to make it compile”
A new teammate fixed an unreported exception IOException error by adding throws Exception to the method, then to its caller, then to every method up to main. It compiles now. What feedback do you give?
The interviewer is checking: do you know the purpose of the rule and the options for satisfying it?
Show a strong answer
throws Exceptionhides which problems can really happen, and it forces every caller to deal with “anything”.- Declare the specific type (
throws IOException) only as far up as needed, and catch it at the first level that can do something useful: retry, fallback, or a clear error message. - If no caller can recover, wrap it once in an unchecked exception with the cause and a meaningful message.
Avoid: treating compile errors as obstacles to silence. The rule is asking for a plan.
Checked or unchecked for “insufficient funds”?
You are designing a transfer(from, to, amount) method for a banking library used by several teams. What exception types would you use for (a) insufficient balance, (b) a negative amount, (c) the core banking server being unreachable?
The interviewer is checking: can you match the exception kind to the situation and justify it?
Show a strong answer
- (b) Negative amount: the caller's bug. Unchecked
IllegalArgumentException; validate before calling. - (a) Insufficient balance: an expected business outcome every caller must handle. A custom exception (Lesson 9.5); checked makes it impossible to forget, unchecked is the more common modern style if the team documents it well. Some APIs return a result object instead.
- (c) Server unreachable: an outside failure. Often a checked exception or a library-specific unchecked one; either way, callers need to know they can retry.
- Whatever you choose, keep it consistent across the library and document it.
Avoid: one generic Exception for all three. Callers can't tell a bug from a retryable outage.
Checked exception inside a stream
Code reads a list of report file paths and calls paths.stream().map(p -> Files.readString(p)). It doesn't compile. A colleague suggests catching the exception inside the lambda and returning an empty string. Is that a good idea?
The interviewer is checking: do you understand why lambdas reject checked exceptions and how to wrap safely?
Show a strong answer
Function.applydeclares no checked exceptions, soIOExceptioncan't escape the lambda.- Returning
""silently turns a missing report into an empty one: the totals will be wrong and nobody will notice. - Better: catch and rethrow
new UncheckedIOException("Cannot read " + p, e), which keeps the cause and stops the pipeline; or collect failures explicitly and report them.
Avoid: “fixing” compile errors by swallowing exceptions.
The log says only “Load failed”
Production logs for a failing config loader show only java.lang.RuntimeException: Load failed and a short trace with no further detail. The code is catch (IOException e) { throw new RuntimeException("Load failed"); }. What went wrong and how do you fix it?
The interviewer is checking: do you know how to wrap exceptions without losing information?
Show a strong answer
- The wrapper was created without the cause, so the original IOException (file name, permission denied, disk error) and its stack trace were thrown away.
- Fix:
throw new UncheckedIOException("Load failed for " + path, e). Now the trace shows a “Caused by:” section with the real reason. - Include the useful context (which file, which tenant) in the wrapper's message.
Avoid: new RuntimeException(e.getMessage()): it keeps only the text and loses the type and stack trace.