The admission form that accepted anything
Riya builds the admission form for her college's evening course. A tester types age -5 and percentage 104. The program prints “Admitted” and stores both values. Nothing crashed, which is exactly the problem: wrong data went straight into the records.
Sir, Java throws exceptions when I divide by zero. But age −5 is a perfectly valid int, so Java doesn't complain.
Right. Java knows the rules of int; it doesn't know the rules of your college. For those, you throw: throw new IllegalArgumentException("Age must be 17-30, got " + age);
And what's throws with an s? I saw it in method headers.
throw is an action: “throw this object now.” throws is a label on a method: “a checked exception of this type may come out of me.” One letter, two very different jobs.
What are throw and throws?
The throw statement (throw expression;) throws the exception object given by the expression, which must be of type Throwable or a subclass. Execution of the current block stops immediately and the JVM begins searching for a handler. The throws clause, written in a method or constructor declaration (void read() throws IOException), lists the checked exception types that the method may throw to its caller. It is required for checked exceptions the method does not handle, and optional (documentation only) for unchecked ones.
throw = raising the alarm right now, with a note saying what's wrong. throws = a warning sign on the door of a method: “this alarm may go off in here; be ready.”
Words you'll meet in this lesson
throw e; inside a catch: pass the same object on.throw vs throws
throw | throws | |
|---|---|---|
| What it is | A statement | Part of a method or constructor header |
| Where | Inside a method body | After the parameter list |
| Followed by | One exception object: throw new X("msg"); | One or more types: throws IOException, SQLException |
| Effect | Stops the block and starts the handler search now | Nothing at run time; tells callers and javac what may come out |
| Required? | Whenever you want to signal a problem | For checked exceptions the method doesn't catch |
Exam tip: any statement directly after throw in the same block is a compile error: unreachable statement. And throw needs a Throwable: throw "error"; does not compile.
Four ways you'll use them
Validate and throw
Check arguments at the start of a method and throw IllegalArgumentException or IllegalStateException with a clear message.
Declare and pass up
Can't handle a checked exception here? Add throws and let a caller that knows what to do catch it.
Rethrow or chain
Catch to log or add context, then throw e; (same object) or throw new X("context", e); (new object, old one as cause).
import java.io.IOException;
public class Main {
// throw: create one exception object and throw it now
static void setQuantity(int qty) {
if (qty <= 0) {
throw new IllegalArgumentException("Quantity must be positive: " + qty);
}
System.out.println("Quantity set to " + qty);
}
// throws: declare that a checked exception may come out of this method
static void saveCart(boolean diskFull) throws IOException {
if (diskFull) {
throw new IOException("Cart file could not be written");
}
System.out.println("Cart saved");
}
public static void main(String[] args) {
try {
setQuantity(3);
setQuantity(0);
} catch (IllegalArgumentException e) {
System.out.println("Caught: " + e.getMessage());
}
try {
saveCart(false);
saveCart(true);
} catch (IOException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}Which built-in exception should I throw?
| Problem | Throw | Example message |
|---|---|---|
| An argument has a bad value | IllegalArgumentException | "Age must be 17-30, got 15" |
| A required argument is null | NullPointerException via Objects.requireNonNull(x, "msg") | "Customer name must not be null" |
| The object can't do this right now | IllegalStateException | "Account is closed" |
| The operation isn't supported at all | UnsupportedOperationException | "Refunds are not allowed for gift cards" |
| A business rule with its own data | Your own class (Lesson 9.5) | InsufficientFundsException with a shortfall |
Overriding rule: an overriding method may throw the same checked exceptions as the parent's method, narrower ones (subclasses) or none, but not broader or new checked ones. If Report.export() declares throws IOException, a subclass declaring throws Exception fails: overridden method does not throw Exception. Unchecked exceptions are never restricted.
From a broken rule to a handled exception
Check the rule and throw
if (age < 17) { throw new IllegalArgumentException("Minimum age is 17, got " + age); }newcreates the exception object on the heap; its constructor records the message and fills in the stack trace of this moment. Theathrowbytecode instruction then throws it. The stack trace points to the line withnew.Nothing after the throw runs
The rest of the method is skipped, and so is everything after the call in each caller that doesn't catch it.
That's why validation goes at the start of a method: if a rule fails, no data has been changed yet. This style is called fail fast.Declare checked exceptions you don't handle
The throws clause is stored in the class file (anstatic void saveCart(boolean diskFull) throws IOException { … }Exceptionsattribute) so that javac can check every caller, even callers compiled later in other projects.Let it propagate to the right level
The method that can do something sensible (show a message, skip a record, retry) is where the catch belongs, often several frames up.
Each frame without a matching handler is popped. Methods in between need no code at all for unchecked exceptions, and only athrowsclause for checked ones.Add context: rethrow or chain
catch (IllegalStateException e) { throw new RuntimeException("Order " + orderId + " failed", e); }throw e;rethrows the same object, keeping its original stack trace.new X(msg, e)creates a second object whosecausefield points to the first; stack traces print both, the inner one under “Caused by:”.
Thrown, caught, chained, caught again: both views
Left: the line and the console. Right: the frames on the stack and the exception objects on the heap.
- Calls: main calls
placeOrder(9001, 5), which callsreserve(5). - throw: 5 is more than the 2 units in stock, so
reservethrows anIllegalStateExceptionwith the message “Only 2 units left”. - catch:
placeOrderhas a catch for that type, so the exception stops there for a moment. - Chain: placeOrder throws a new
RuntimeException("Order 9001 failed", e). The first exception travels inside it as its cause. - Caught again: main's catch receives the order-level exception and prints its message.
- Root cause:
getCause()gives back the stock exception, so the real reason can be logged.
- Each call pushed a frame: main at the bottom, placeOrder in the middle, reserve on top.
new IllegalStateException(…)allocates object #1 and records the stack (reserve, placeOrder, main).throwstarts the handler search in reserve.- reserve has no handler, so its frame is popped. placeOrder's exception table has a matching row: its catch parameter now refers to #1.
- The catch block creates #2 with cause = #1 and throws it. placeOrder's own catch doesn't cover code inside the catch block, so the search continues in main.
- placeOrder's frame is popped. main's catch matches #2.
getCause()just returns the reference stored in #2. Nothing was lost by wrapping.
throw and throws in real systems
Throwing is how real code says “no” clearly, at the right place, with the right information.
Watch it with an example: a KYC form with a wrong PAN
A bank's account-opening form passes through three methods. The innermost one checks the PAN number and throws; watch the exception pass through the middle layer and get caught where the screen can show a message.
- Three layers: the screen's
submitcallsopenAccount, which callscheckPan. Onlysubmithas a try/catch. - throw: a PAN has 10 characters; "ABCD1234" has 8.
checkPancreates anIllegalArgumentExceptionwith a clear message and throws it. - Popped:
checkPanhas no catch, so its frame is removed and the exception moves to its caller. - Passed through:
openAccounthas no catch either. Its "save account" line is skipped, so no half-created account exists. - Caught:
submit's catch matches. Because the exception is unchecked, the two methods in between needed nothrowsclause. - Result: the message written in the validator reaches the screen unchanged. One rule, one throw, one place that shows errors.
1. Validating requests
A sign-up or KYC service checks mobile number, PAN format, age and so on at the start of the method and throws on the first broken rule. A web-layer handler turns it into a 400 response with the message.
2. Protecting business state
Wallets, carts and orders throw IllegalStateException for operations that don't fit the current state: paying for a cancelled order, refunding twice, shipping an unpaid order.
3. Layers that add context
The repository says “connection refused”; the service wraps it as “could not load order 501”; the controller logs the whole chain once. Each layer adds what only it knows.
4. Library APIs that document failures
Library methods list checked exceptions in throws and document unchecked ones, so users of the library know what to prepare for without reading its code.
Examples are simplified. Large applications often define their own exception classes for business rules (Lesson 9.5) and handle them in one central place.
Riya's admission form, with validation
public class Main {
static void validate(String name, int age, double percent) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name is required");
}
if (age < 17 || age > 30) {
throw new IllegalArgumentException("Age must be 17-30, got " + age);
}
if (percent < 0 || percent > 100) {
throw new IllegalArgumentException("Percentage must be 0-100, got " + percent);
}
}
static void admit(String name, int age, double percent) {
validate(name, age, percent);
System.out.println("Admitted: " + name);
}
public static void main(String[] args) {
String[] names = {"Riya", "", "Kabir", "Sana"};
int[] ages = {18, 19, 16, 20};
double[] percents = {88.5, 72.0, 91.0, 104.0};
for (int i = 0; i < names.length; i++) {
try {
admit(names[i], ages[i], percents[i]);
} catch (IllegalArgumentException e) {
System.out.println("Form " + (i + 1) + " rejected: " + e.getMessage());
}
}
}
}throw new IllegalArgumentException(…)Each rule throws with a message that includes the bad value.name.isBlank()True for "" or only spaces (Java 11+). The null check comes first, so isBlank is never called on null.admitHas no try: the exception passes straight through it, skipping “Admitted”.catchin mainOne place decides what a rejection looks like for the user.
An exception passes through three frames
The coupon check throws in the innermost method. Watch applyCoupon and checkout being popped without finishing, until main's catch takes it.
Worked examples: real problems, solved and traced in memory
Three business rules enforced with throw. Run each solution, then step through it: watch the exception object being created by your own throw, the frames it passes through, and, in the last example, a second exception that keeps the first as its cause.
Reject an invalid age at admission
Problem statement
A college admission system must refuse applicants younger than 17 or with an impossible age. validateAge throws an IllegalArgumentException; register calls it; main handles each applicant separately.
applicants: ("Aarav", 18), ("Diya", 15)Registered Aarav Diya not registered: Minimum age is 17, got 15
Approach
validateAgechecks the rule and usesthrow new IllegalArgumentException(…)when it is broken.registerdoes not catch, so the exception passes through it and its "Registered" line is skipped.mainhas a try per applicant, so one rejection doesn't stop the next applicant.
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.
Log in the service, rethrow to the caller
Problem statement
A fund transfer debits the sender first. If the balance is too low, debit throws. The transfer service logs the failure with the transfer details and rethrows the same exception so the app screen can show its own message.
sender balance = Rs 3,000 transfer Rs 5,000 to account 7781
[log] transfer to 7781 failed: Balance 3000 is less than 5000 Screen: Transfer could not be completed Balance: Rs 3000
Approach
debitthrowsIllegalStateExceptionwhen the balance is too low.transfercatches it only to add a log line, then writesthrow e;to pass the very same object on.main(the app screen) catches it and shows a customer-friendly message.
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.
Chain the stock error to the order error
Problem statement
Placing an order reserves stock. If the warehouse has too few units, reserve throws. placeOrder catches that and throws a new, higher-level exception about the order, keeping the stock error as its cause. main prints both.
stock of item = 2 units order 9001 asks for 5 units
Order 9001 failed Cause: Only 2 units left
Approach
reservethrows anIllegalStateExceptiondescribing the stock problem.placeOrderwraps it:new RuntimeException("Order … failed", e). The order-level message is for the user; the cause is for the developer.mainreadsgetMessage()andgetCause().
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.4 in one screen
throw new X("message");throws one object now; nothing after it in the block runs.throws X, Yin a header lists exceptions a method may pass to callers; required for unhandled checked ones.- Validate at the start of a method (fail fast) with IllegalArgumentException / IllegalStateException / requireNonNull.
- An uncaught exception pops each frame until a matching catch is found.
throw e;rethrows the same object;new X(msg, e)chains it as a cause.- Walk causes with
getCause(); traces show them under “Caused by:”. - Overrides may throw the same, narrower or no checked exceptions, never broader.
Interview and viva questions
What is the difference between throw and throws?
throw is a statement inside a method body that throws one exception object at that moment. throws is a clause in a method header declaring which exception types may leave the method. throw acts at run time; throws informs the compiler and callers.
Can we throw a checked exception without declaring it?
Only if it is caught within the same method. Otherwise javac reports “unreported exception … must be caught or declared to be thrown”. Unchecked exceptions can always be thrown without a declaration.
What is exception chaining and why is it useful?
Creating a new exception with an earlier one as its cause, via the (message, cause) constructor or initCause. Each layer can describe the failure in its own terms while the original type, message and stack trace stay available through getCause() and the “Caused by:” section.
Why can't an overriding method throw a broader checked exception?
Callers using a parent-class reference only prepared for the exceptions the parent method declares. If a subclass could throw something broader, that code would receive a checked exception it never planned for, breaking the substitution principle. So javac only allows the same, narrower or no checked exceptions.
Is it a good idea to catch an exception just to log and rethrow it?
Sometimes, when that layer adds context nobody else has. But if every layer logs and rethrows, one failure shows up many times in the logs. A common rule: either handle it, or add context and pass it on, and log it once at the boundary.
Practice problems
Practise throwing your own exceptions for bad input, declaring checked ones with throws, letting them travel up the call stack, rethrowing and chaining them. The last three are real business tasks.
Reject impossible marks
Write setMarks(int marks) that throws an IllegalArgumentException when the marks are outside 0 to 100, and returns the grade letter otherwise. Test it with three values.
marks = 82, 105, -3
82 -> grade A Rejected: Marks must be 0-100, got 105 Rejected: Marks must be 0-100, got -3
Show solution and explanation
public class Main {
static char setMarks(int marks) {
if (marks < 0 || marks > 100) {
throw new IllegalArgumentException("Marks must be 0-100, got " + marks);
}
return marks >= 80 ? 'A' : marks >= 60 ? 'B' : 'C';
}
public static void main(String[] args) {
int[] tests = {82, 105, -3};
for (int m : tests) {
try {
System.out.println(m + " -> grade " + setMarks(m));
} catch (IllegalArgumentException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}
}How it works: throw takes an exception object, so we create one with new and a message that includes the bad value. The method stops at the throw; the return line runs only for valid marks. IllegalArgumentException is the standard unchecked exception for “you passed me a bad value”.
Wrong state, not wrong argument
A bank account can be closed. Withdrawing from a closed account is not a bad amount; the account is in the wrong state. Throw IllegalStateException in that case.
open account withdraw 500; closed account withdraw 500
New balance: 1500 Blocked: Account is closed
Show solution and explanation
public class Main {
static int withdraw(boolean closed, int balance, int amount) {
if (closed) {
throw new IllegalStateException("Account is closed");
}
return balance - amount;
}
public static void main(String[] args) {
try {
System.out.println("New balance: " + withdraw(false, 2000, 500));
System.out.println("New balance: " + withdraw(true, 2000, 500));
} catch (IllegalStateException e) {
System.out.println("Blocked: " + e.getMessage());
}
}
}How it works: pick the exception class that describes the problem. IllegalArgumentException means the input is wrong; IllegalStateException means the input may be fine but the object can't accept it right now. Readers of the stack trace learn something just from the class name.
Declare a checked exception
Write loadDriver(String name) that declares throws ClassNotFoundException instead of catching it. Call it from main for an existing and a missing class.
"java.lang.String" and "com.fake.Driver"
Loaded java.lang.String Not found: com.fake.Driver
Show solution and explanation
public class Main {
static void loadDriver(String name) throws ClassNotFoundException {
Class.forName(name);
System.out.println("Loaded " + name);
}
public static void main(String[] args) {
try {
loadDriver("java.lang.String");
loadDriver("com.fake.Driver");
} catch (ClassNotFoundException e) {
System.out.println("Not found: " + e.getMessage());
}
}
}How it works: throws sits in the method header and tells callers which checked exceptions can come out of it. loadDriver does no handling; it just passes the problem up. main handles it. Note the difference: throw (a statement) throws one object now; throws (a declaration) lists types that may come out.
Watch it travel
Three methods call each other. The deepest one throws. Print a message when each method starts and when it ends normally, and predict which “end” messages appear.
main → level1 → level2 → level3 (throws)
level1 start level2 start level3 start level1 caught: deep problem level1 end main end
Show solution and explanation
public class Main {
static void level3() {
System.out.println("level3 start");
throw new IllegalStateException("deep problem");
}
static void level2() {
System.out.println("level2 start");
level3();
System.out.println("level2 end");
}
static void level1() {
System.out.println("level1 start");
try {
level2();
} catch (IllegalStateException e) {
System.out.println("level1 caught: " + e.getMessage());
}
System.out.println("level1 end");
}
public static void main(String[] args) {
level1();
System.out.println("main end");
}
}How it works: the exception goes up the stack until it meets a matching catch. level3 and level2 are popped without finishing (no “end” messages). level1 catches it, so from there on everything is normal: “level1 end” and “main end” both print. Nothing after the throw in level3 can ever run, which is why javac rejects statements directly after a throw as unreachable.
Log and rethrow
A service method should log every failure with its own context, but still let the caller decide what to do. Catch, log, and rethrow the same exception with throw e;.
order ids "A101" (ok) and "" (empty)
Order A101 [service log] fetch failed: Order id is empty [ui] Please enter an order id
Show solution and explanation
public class Main {
static String fetchOrder(String id) {
if (id.isEmpty()) {
throw new IllegalArgumentException("Order id is empty");
}
return "Order " + id;
}
static String orderService(String id) {
try {
return fetchOrder(id);
} catch (IllegalArgumentException e) {
System.out.println("[service log] fetch failed: " + e.getMessage());
throw e;
}
}
public static void main(String[] args) {
String[] ids = {"A101", ""};
for (String id : ids) {
try {
System.out.println(orderService(id));
} catch (IllegalArgumentException e) {
System.out.println("[ui] Please enter an order id");
}
}
}
}How it works: throw e; throws the same object again, with its original stack trace. The service adds a log line, and the UI shows a friendly message. Be careful in real systems: if every layer logs and rethrows, one failure appears many times in the logs. Usually only one layer should log.
Follow the cause chain
A low-level error is wrapped twice as it goes up. In main, walk the chain with getCause() and print each level, then print the root cause on its own.
IOException → IllegalStateException → RuntimeException
level 0: java.lang.RuntimeException: Checkout failed for order 501 level 1: java.lang.IllegalStateException: Invoice not saved level 2: java.io.IOException: Disk full root cause: Disk full
Show solution and explanation
import java.io.IOException;
public class Main {
static void writeFile() throws IOException {
throw new IOException("Disk full");
}
static void saveInvoice() {
try {
writeFile();
} catch (IOException e) {
throw new IllegalStateException("Invoice not saved", e);
}
}
static void checkout() {
try {
saveInvoice();
} catch (IllegalStateException e) {
throw new RuntimeException("Checkout failed for order 501", e);
}
}
public static void main(String[] args) {
try {
checkout();
} catch (RuntimeException e) {
Throwable t = e;
int level = 0;
while (t != null) {
System.out.println("level " + level + ": " + t);
if (t.getCause() == null) {
System.out.println("root cause: " + t.getMessage());
}
t = t.getCause();
level++;
}
}
}
}How it works: each layer adds context in its own words and passes the earlier exception as the cause. The chain is a linked list of exception objects; getCause() returns null at the end. The top message tells the user what failed; the root cause tells the developer why.
Null checks with a clear message
Objects.requireNonNull(value, message) throws a NullPointerException with your message when the value is null. Use it to validate a customer name at the start of a method.
names "Meera" and null
Welcome, Meera Error: Customer name must not be null
Show solution and explanation
import java.util.Objects;
public class Main {
static String greet(String name) {
Objects.requireNonNull(name, "Customer name must not be null");
return "Welcome, " + name;
}
public static void main(String[] args) {
System.out.println(greet("Meera"));
try {
System.out.println(greet(null));
} catch (NullPointerException e) {
System.out.println("Error: " + e.getMessage());
}
}
}How it works: this is a ready-made “if null, throw” check. Failing at the start of the method, with a message naming the parameter, is far easier to debug than a NullPointerException deep inside later code. This style is called fail fast.
Overriding and throws
A parent class's method declares throws IOException. One subclass declares a narrower FileNotFoundException, another declares nothing. Call both through a parent reference.
exporters: CsvExport (file missing), ScreenExport (no file needed)
Export failed: java.io.FileNotFoundException: report.csv shown on screen
Show solution and explanation
import java.io.FileNotFoundException;
import java.io.IOException;
public class Main {
static class Exporter {
void export() throws IOException {
System.out.println("generic export");
}
}
static class CsvExport extends Exporter {
@Override
void export() throws FileNotFoundException {
throw new FileNotFoundException("report.csv");
}
}
static class ScreenExport extends Exporter {
@Override
void export() {
System.out.println("shown on screen");
}
}
public static void main(String[] args) {
Exporter[] exporters = {new CsvExport(), new ScreenExport()};
for (Exporter ex : exporters) {
try {
ex.export();
} catch (IOException e) {
System.out.println("Export failed: " + e);
}
}
}
}How it works: an overriding method may throw the same checked exceptions, narrower ones, or none, but never broader or new checked ones. Code written against Exporter only prepared for IOException; a subclass throwing Exception would break that promise, so javac rejects it with “overridden method does not throw Exception”. Unchecked exceptions are not restricted.
ATM withdrawal rules
Validate an ATM withdrawal: the amount must be positive and a multiple of 100, must not exceed the balance, and must not exceed the daily limit of Rs 20,000. Throw a fitting exception for each rule and process a list of requests.
balance = Rs 50,000; requests = 5000, 250, 30000, 12000, 9000
Rs 5000 dispensed, balance Rs 45000 Rs 250 refused: Amount must be a positive multiple of 100 Rs 30000 refused: Daily limit reached, Rs 15000 left today Rs 12000 dispensed, balance Rs 33000 Rs 9000 refused: Daily limit reached, Rs 3000 left today
Show solution and explanation
public class Main {
static int balance = 50000;
static int withdrawnToday = 0;
static final int DAILY_LIMIT = 20000;
static void withdraw(int amount) {
if (amount <= 0 || amount % 100 != 0) {
throw new IllegalArgumentException("Amount must be a positive multiple of 100");
}
if (amount > balance) {
throw new IllegalStateException("Insufficient balance (Rs " + balance + ")");
}
if (withdrawnToday + amount > DAILY_LIMIT) {
throw new IllegalStateException("Daily limit reached, Rs " + (DAILY_LIMIT - withdrawnToday) + " left today");
}
balance -= amount;
withdrawnToday += amount;
}
public static void main(String[] args) {
int[] requests = {5000, 250, 30000, 12000, 9000};
for (int r : requests) {
try {
withdraw(r);
System.out.println("Rs " + r + " dispensed, balance Rs " + balance);
} catch (IllegalArgumentException | IllegalStateException e) {
System.out.println("Rs " + r + " refused: " + e.getMessage());
}
}
}
}How it works:
- All checks happen before any money moves; a throw stops the method, so the balance is never left half-updated.
- Bad input (250) gets
IllegalArgumentException; a valid amount the account can't serve getsIllegalStateException. - The messages carry the numbers the customer needs. Lesson 9.5 shows how a custom exception can carry them as fields too.
Delivery order validation
Before an order is accepted, validate the quantity (1 to 10 per item) and the delivery PIN code (exactly 6 digits, not starting with 0). Validation lives in its own method called from placeOrder; main reports accepted and rejected orders.
orders: (2, "411001"), (0, "560001"), (3, "4110"), (1, "012345")
Order 101 accepted for PIN 411001 Order 102 rejected: Quantity must be 1-10, got 0 Order 103 rejected: Invalid PIN code: 4110 Order 104 rejected: Invalid PIN code: 012345 1 of 4 orders accepted
Show solution and explanation
public class Main {
static void validate(int qty, String pin) {
if (qty < 1 || qty > 10) {
throw new IllegalArgumentException("Quantity must be 1-10, got " + qty);
}
if (!pin.matches("[1-9][0-9]{5}")) {
throw new IllegalArgumentException("Invalid PIN code: " + pin);
}
}
static String placeOrder(int id, int qty, String pin) {
validate(qty, pin);
return "Order " + id + " accepted for PIN " + pin;
}
public static void main(String[] args) {
int[] qtys = {2, 0, 3, 1};
String[] pins = {"411001", "560001", "4110", "012345"};
int accepted = 0;
for (int i = 0; i < qtys.length; i++) {
try {
System.out.println(placeOrder(101 + i, qtys[i], pins[i]));
accepted++;
} catch (IllegalArgumentException e) {
System.out.println("Order " + (101 + i) + " rejected: " + e.getMessage());
}
}
System.out.println(accepted + " of " + qtys.length + " orders accepted");
}
}How it works:
- The exception is thrown in
validate, passes throughplaceOrderwithout being caught, and lands in main: two frames up. placeOrderstays clean: no ifs for each rule, no error codes to return.- The regular expression
[1-9][0-9]{5}means “a digit 1 to 9, then exactly five digits”.
Payment retries, then a chained failure
Charge a card through a flaky gateway, retrying up to 3 times. If all attempts fail, throw one IllegalStateException that says how many attempts were made and carries the last gateway error as its cause.
amount = Rs 2,499; the gateway fails every time in this run
Attempt 1 failed Attempt 2 failed Attempt 3 failed Payment failed after 3 attempts Last error: Gateway timeout on call 3
Show solution and explanation
public class Main {
static int calls = 0;
static void gatewayCharge(int amount) {
calls++;
throw new RuntimeException("Gateway timeout on call " + calls);
}
static void payWithRetry(int amount, int maxAttempts) {
RuntimeException last = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
gatewayCharge(amount);
return;
} catch (RuntimeException e) {
System.out.println("Attempt " + attempt + " failed");
last = e;
}
}
throw new IllegalStateException("Payment failed after " + maxAttempts + " attempts", last);
}
public static void main(String[] args) {
try {
payWithRetry(2499, 3);
System.out.println("Paid");
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
System.out.println("Last error: " + e.getCause().getMessage());
}
}
}How it works:
- Inside the loop, each failure is caught and remembered, so the loop can try again.
returninside try leaves the method on the first success.- After the loop, one clear exception reports the overall failure, chained to the last real error, so no detail is lost and the caller sees a single, meaningful problem.
Real-world scenario questions (asked in company interviews)
Where you throw, what you throw and what you put in the message decide how fast a problem gets fixed. Decide your answer first, then open the model answer.
A negative top-up that created money
A wallet's topUp(amount) simply does balance += amount. A buggy partner app sent -500 as a top-up, and later a refund flow sent a top-up for a closed wallet. How would you change topUp?
The interviewer is checking: do you validate inputs and state, and pick the right exception?
Show a strong answer
- Check at the start:
if (amount <= 0) throw new IllegalArgumentException("Top-up must be positive, got " + amount); - Check state:
if (closed) throw new IllegalStateException("Wallet " + id + " is closed"); - Only after all checks pass, change the balance, so a failure never leaves the wallet half-updated.
- Add tests for both cases, and log the partner id so the faulty client can be found.
Avoid: silently ignoring bad values or returning false that callers forget to check.
“Invalid input” and nothing else
Support tickets say checkout fails with “Invalid input”. The code has eight checks, each doing throw new IllegalArgumentException("Invalid input"). The logs don't help either. What would you change?
The interviewer is checking: do you write messages that make problems diagnosable?
Show a strong answer
- Make every message say which rule failed and the actual value:
"PIN code must be 6 digits, got '4110'". - Include identifiers (order id, user id) where safe, but never passwords, full card numbers or OTPs.
- Map messages for the user separately (friendly text) from messages for logs (precise text).
Avoid: generic messages. The exception is often the only clue a developer gets.
The root cause vanished
A loan service's logs show LoanException: Could not compute EMI with no further detail. In the code: catch (Exception e) { throw new LoanException("Could not compute EMI"); }. How do you fix it so the next incident is easy to debug?
The interviewer is checking: do you know to chain the cause when rethrowing?
Show a strong answer
- Pass the original exception:
throw new LoanException("Could not compute EMI for loan " + loanId, e);and make sure LoanException has a(String, Throwable)constructor. - Now the log shows “Caused by: …” with the real exception (a null rate, a division by zero, a database timeout).
- Narrow
catch (Exception e)to the exceptions this code expects, so real bugs aren't disguised as business failures.
Avoid: new LoanException(e.getMessage()): it keeps the text but loses the type and stack trace.
A plugin that must throw more
Your company's reporting library has interface Exporter { void export(Report r) throws IOException; }. A team writing a cloud exporter needs to throw a checked CloudAuthException (not an IOException). They can't compile. What are the options?
The interviewer is checking: do you understand the throws rule for overriding and its design consequences?
Show a strong answer
- The rule exists because callers of
Exporteronly handle IOException; a new checked type would surprise them. - Option 1: make CloudAuthException extend IOException if it really is an I/O-style failure.
- Option 2: wrap it:
throw new IOException("Cloud auth failed", e), keeping the cause. - Option 3 (library change, new major version): declare a library-specific
ExportExceptionthat all exporters wrap their failures in.
Avoid: converting everything to unchecked RuntimeException without telling callers. They lose the information that export can fail.