throw and throws

Until now the JVM threw exceptions for you. Now you'll throw your own: refuse an age of 15, a withdrawal above the balance, a coupon that doesn't exist. You'll learn the difference between throw and throws, follow an exception through several method frames, rethrow it after logging, chain causes so no detail is lost, see the rules for throws when overriding, and then practise with 11 problems.

Module 9 · Exception Handling⏱ 23 minIntermediate11 practice problems
1 The story

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.

Admission form NameRiya Age-5 Percent104 validate(form) if (age < 17) throw … valid: save records IllegalArgumentException: “Age must be 17-30, got -5” myphatshala.in
Riya

Sir, Java throws exceptions when I divide by zero. But age −5 is a perfectly valid int, so Java doesn't complain.

Arjun Sir

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);

Riya

And what's throws with an s? I saw it in method headers.

Arjun Sir

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.

2 Definition

What are throw and throws?

Formal definition (write this in exams)

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.

In simple words

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

throwA statement that throws one exception object.
throwsA clause in a method header listing exception types.
ValidationChecking input against rules before using it.
PropagationAn uncaught exception moving to the caller's frame.
Rethrowthrow e; inside a catch: pass the same object on.
ChainingThrowing a new exception with the old one as its cause.

throw vs throws

throwthrows
What it isA statementPart of a method or constructor header
WhereInside a method bodyAfter the parameter list
Followed byOne exception object: throw new X("msg");One or more types: throws IOException, SQLException
EffectStops the block and starts the handler search nowNothing at run time; tells callers and javac what may come out
Required?Whenever you want to signal a problemFor 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.

3 Types and ways to use it

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.

throw new …

Declare and pass up

Can't handle a checked exception here? Add throws and let a caller that knows what to do catch it.

throws IOException

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).

throw e / 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());
        }
    }
}
Quantity set to 3 Caught: Quantity must be positive: 0 Cart saved Caught: Cart file could not be written

Which built-in exception should I throw?

ProblemThrowExample message
An argument has a bad valueIllegalArgumentException"Age must be 17-30, got 15"
A required argument is nullNullPointerException via Objects.requireNonNull(x, "msg")"Customer name must not be null"
The object can't do this right nowIllegalStateException"Account is closed"
The operation isn't supported at allUnsupportedOperationException"Refunds are not allowed for gift cards"
A business rule with its own dataYour 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.

4 Step by step

From a broken rule to a handled exception

  1. Check the rule and throw

    if (age < 17) {
        throw new IllegalArgumentException("Minimum age is 17, got " + age);
    }
    new creates the exception object on the heap; its constructor records the message and fills in the stack trace of this moment. The athrow bytecode instruction then throws it. The stack trace points to the line with new.
  2. 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.
  3. Declare checked exceptions you don't handle

    static void saveCart(boolean diskFull) throws IOException { … }
    The throws clause is stored in the class file (an Exceptions attribute) so that javac can check every caller, even callers compiled later in other projects.
  4. 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 a throws clause for checked ones.
  5. 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 whose cause field points to the first; stack traces print both, the inner one under “Caused by:”.
5 See it happen

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.

Order 9001 asks for 5 units; only 2 are in stock 6 steps
What you see placeOrder(9001, 5) → reserve(5) throw new IllegalStateException(…); catch (IllegalStateException e) throw new RuntimeException(msg, e); catch (RuntimeException e) // main e.getCause().getMessage() // nothing printed yet Order 9001 failed Cause: Only 2 units left Three methods are running. Our own rule fails: we throw. placeOrder catches the stock problem. …and throws an order-level exception. main catches it and tells the user. The root reason is still available. myphatshala.in
Behind the screen Stack Heap reserve(qty=5) reserve popped placeOrder(9001) placeOrder e→#1 placeOrder popped main(args) main e→#2 #1 IllegalStateException "Only 2 units left" #2 RuntimeException "Order 9001 failed" cause → #1 Three frames; nothing on the heap yet. new creates #1 with message and stack trace. reserve popped; placeOrder's e refers to #1. #2 created; its cause field points to #1. placeOrder popped; main's e refers to #2. getCause() follows #2's cause to #1. myphatshala.in
6 Real-world use

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.

throw in the validator, catch in the controller 6 steps
Animated example submit(form) → openAccount() → checkPan() throw new IllegalArgumentException(…); // checkPan has no catch: frame popped // openAccount has no catch: popped catch (IllegalArgumentException e) screen.showError(e.getMessage()); Stack checkPan("ABCD1234") popped openAccount(form) popped submit(form) try/catch IllegalArgumentException PAN must be 10 characters Open account PAN: ABCD1234 PAN must be 10 characters Three frames: each method waits for the next. 8 characters: the validator throws. The exception skips the rest of checkPan. openAccount never saves the account. submit's catch matches the type. The customer sees why; nothing was saved. myphatshala.in

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.

Request→validate() throws IllegalArgumentException→400 Bad Request
₹?

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.

cancel → pay→throw new IllegalStateException→No double payment

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.

DB error→new ServiceException(msg, e)→One log with full chain
void read()throws IOException

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.

Method header→throws IOException→Caller plans for it

Examples are simplified. Large applications often define their own exception classes for business rules (Lesson 9.5) and handle them in one central place.

7 Run the code

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());
            }
        }
    }
}
Admitted: Riya Form 2 rejected: Name is required Form 3 rejected: Age must be 17-30, got 16 Form 4 rejected: Percentage must be 0-100, got 104.0
  • 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”.
  • catch in mainOne place decides what a rejection looks like for the user.
8 Line by line

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.

Example 1Education · admissions

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.

Input
applicants: ("Aarav", 18), ("Diya", 15)
Expected output
Registered Aarav
Diya not registered: Minimum age is 17, got 15

Approach

  1. validateAge checks the rule and uses throw new IllegalArgumentException(…) when it is broken.
  2. register does not catch, so the exception passes through it and its "Registered" line is skipped.
  3. main has 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.

Example 2Banking · transfers

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.

Input
sender balance = Rs 3,000
transfer Rs 5,000 to account 7781
Expected output
[log] transfer to 7781 failed: Balance 3000 is less than 5000
Screen: Transfer could not be completed
Balance: Rs 3000

Approach

  1. debit throws IllegalStateException when the balance is too low.
  2. transfer catches it only to add a log line, then writes throw e; to pass the very same object on.
  3. 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.

Example 3E-commerce · orders

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.

Input
stock of item = 2 units
order 9001 asks for 5 units
Expected output
Order 9001 failed
Cause: Only 2 units left

Approach

  1. reserve throws an IllegalStateException describing the stock problem.
  2. placeOrder wraps it: new RuntimeException("Order … failed", e). The order-level message is for the user; the cause is for the developer.
  3. main reads getMessage() and getCause().

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.

9 Quiz and revision

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, Y in 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.

P1Easy

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.

Input
marks = 82, 105, -3
Expected output
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());
            }
        }
    }
}
82 -> grade A Rejected: Marks must be 0-100, got 105 Rejected: Marks must be 0-100, got -3

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”.

P2Easy

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.

Input
open account withdraw 500; closed account withdraw 500
Expected output
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());
        }
    }
}
New balance: 1500 Blocked: Account is closed

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.

P3Easy

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.

Input
"java.lang.String" and "com.fake.Driver"
Expected output
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());
        }
    }
}
Loaded java.lang.String Not found: com.fake.Driver

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.

P4Medium

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.

Input
main → level1 → level2 → level3 (throws)
Expected output
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");
    }
}
level1 start level2 start level3 start level1 caught: deep problem level1 end 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.

P5Medium

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;.

Input
order ids "A101" (ok) and "" (empty)
Expected output
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");
            }
        }
    }
}
Order A101 [service log] fetch failed: Order id is empty [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.

P6Medium

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.

Input
IOException → IllegalStateException → RuntimeException
Expected output
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++;
            }
        }
    }
}
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

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.

P7Medium

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.

Input
names "Meera" and null
Expected output
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());
        }
    }
}
Welcome, Meera Error: Customer name must not be null

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.

P8Medium

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.

Input
exporters: CsvExport (file missing), ScreenExport (no file needed)
Expected output
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);
            }
        }
    }
}
Export failed: java.io.FileNotFoundException: report.csv shown on screen

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.

P9Business problem

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.

Input
balance = Rs 50,000; requests = 5000, 250, 30000, 12000, 9000
Expected output
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());
            }
        }
    }
}
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

How it works:

  1. All checks happen before any money moves; a throw stops the method, so the balance is never left half-updated.
  2. Bad input (250) gets IllegalArgumentException; a valid amount the account can't serve gets IllegalStateException.
  3. The messages carry the numbers the customer needs. Lesson 9.5 shows how a custom exception can carry them as fields too.
P10Business problem

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.

Input
orders: (2, "411001"), (0, "560001"), (3, "4110"), (1, "012345")
Expected output
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");
    }
}
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

How it works:

  1. The exception is thrown in validate, passes through placeOrder without being caught, and lands in main: two frames up.
  2. placeOrder stays clean: no ifs for each rule, no error codes to return.
  3. The regular expression [1-9][0-9]{5} means “a digit 1 to 9, then exactly five digits”.
P11Business problem

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.

Input
amount = Rs 2,499; the gateway fails every time in this run
Expected output
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());
        }
    }
}
Attempt 1 failed Attempt 2 failed Attempt 3 failed Payment failed after 3 attempts Last error: Gateway timeout on call 3

How it works:

  1. Inside the loop, each failure is caught and remembered, so the loop can try again.
  2. return inside try leaves the method on the first success.
  3. 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.

Fintech · walletsFresher

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.

E-commerce · support1–3 years

“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.

Banking · microservices1–3 years

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.

SaaS · library design3+ years

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 Exporter only 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 ExportException that all exporters wrap their failures in.

Avoid: converting everything to unchecked RuntimeException without telling callers. They lose the information that export can fail.