Custom exceptions

IllegalArgumentException says “something was wrong”. InsufficientFundsException with a shortfall of Rs 2,500 says exactly what, and lets the app react. You'll learn when to extend Exception or RuntimeException, which constructors to write, how to carry data, how to group exceptions into families, when a built-in exception is the better choice, and then practise with 11 problems.

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

The app that read error messages

Riya's team builds a wallet app. When a payment fails, the backend throws IllegalStateException("Insufficient funds: short by Rs 2500"). To show a “Add Rs 2,500” button, the app developer cuts the number out of the message text. One day someone changes the message to “Low balance, need 2500 more” and the button breaks for every user.

Before: data hidden in text IllegalStateException "Insufficient funds: short by Rs 2500" msg.substring(msg.lastIndexOf(" ") + 1) breaks when the wording changes After: a custom exception InsufficientFundsException message: "Short by Rs 2500" shortfall: 2500 e.getShortfall() → 2500 the app reads a field, not text myphatshala.in
Riya

Sir, the button broke because somebody improved an error message. How can a message change break code?

Arjun Sir

Because the code was reading data out of a sentence. Messages are for humans. If the app needs a number, the exception should carry it as a field.

Riya

But IllegalStateException doesn't have a shortfall field.

Arjun Sir

So write your own: class InsufficientFundsException extends Exception with a shortfall field and a getter. Its name tells the whole story, and a catch can pick it out by type.

2 Definition

What is a custom exception?

Formal definition (write this in exams)

A custom (user-defined) exception is a class that extends Exception (making it checked) or RuntimeException (making it unchecked), created to represent an application-specific error condition. It usually provides constructors that pass a message and/or a cause to the superclass with super(...), and may declare additional fields and getters that describe the failure. It is thrown with throw and caught by its type like any other exception.

In simple words

A custom exception is your own named alarm. Its name says what happened (InsufficientFunds, OutOfStock), its message explains it to people, and its fields give code the details it needs to react.

Words you'll meet in this lesson

User-defined exceptionAn exception class you write yourself.
super(message)Passes the message to Exception's constructor, which stores it.
super(message, cause)Stores both the message and the original exception.
Extra fieldData the catch block can use: shortfall, item, error code.
Exception familyA base exception with subclasses, caught together or separately.
serialVersionUIDA version number for serialization; javac -Xlint:serial warns if missing.

Designing one at a glance

Parent classException → checked (callers must handle). RuntimeException → unchecked (callers may handle).
NameA noun ending in Exception that says what happened: InsufficientFundsException, not BankError or MyException
ConstructorsAt least (String message) and (String message, Throwable cause); build the message from fields when they exist
Fieldsprivate final with getters only: exceptions should be immutable
Inherited for freegetMessage(), getCause(), printStackTrace(), toString(), the stack trace
Don't create onewhen a built-in already says it: bad argument → IllegalArgumentException, wrong state → IllegalStateException

Exam tip: “How do you create a user-defined exception?” Answer: extend Exception (checked) or RuntimeException (unchecked), call super(message) in the constructor, and throw it with throw new MyException("…"). Write all three parts in the answer.

3 Types and ways to use it

Checked or unchecked, simple or data-carrying

Checked: extends Exception

For business outcomes every caller must think about: insufficient funds, seat already booked, KYC pending.

compiler enforces

Unchecked: extends RuntimeException

For rule violations usually handled at one boundary: invalid order, expired coupon, limit exceeded. The modern default in many teams.

no throws needed

Data-carrying

Adds fields such as shortfall, item, errorCode or retryable, so callers decide without parsing text.

private final + getter
// Checked: callers must handle it
class KycPendingException extends Exception {
    public KycPendingException(String message) {
        super(message);
    }
}

// Unchecked: carries data, callers may handle it
class DailyLimitException extends RuntimeException {
    private final long remaining;

    public DailyLimitException(long remaining) {
        super("Daily limit reached, Rs " + remaining + " left today");
        this.remaining = remaining;
    }

    public long getRemaining() {
        return remaining;
    }
}

public class Main {
    static void send(boolean kycDone, long amount) throws KycPendingException {
        if (!kycDone) {
            throw new KycPendingException("Complete KYC to send money");
        }
        long left = 10000;
        if (amount > left) {
            throw new DailyLimitException(left);
        }
        System.out.println("Sent Rs " + amount);
    }

    public static void main(String[] args) {
        try {
            send(true, 2000);
            send(false, 500);
        } catch (KycPendingException e) {
            System.out.println("Checked:   " + e);
        }
        try {
            send(true, 15000);
        } catch (KycPendingException e) {
            System.out.println("not reached");
        } catch (DailyLimitException e) {
            System.out.println("Unchecked: " + e);
            System.out.println("Remaining: " + e.getRemaining());
        }
    }
}
Sent Rs 2000 Checked: KycPendingException: Complete KYC to send money Unchecked: DailyLimitException: Daily limit reached, Rs 10000 left today Remaining: 10000

Both classes sit in the same file as Main (only one class per file can be public). send must declare the checked one; the unchecked one needs no declaration. Printing e shows your class's name, which is why a good name matters.

SituationBetter choiceWhy
Negative quantity passed to a methodBuilt-in IllegalArgumentExceptionEveryone already knows what it means
Withdrawal larger than the balanceCustom InsufficientFundsException with shortfallA business case callers handle specially, with data
Item out of stock during checkoutCustom OutOfStockException with item and available unitsThe UI suggests alternatives from the fields
Database driver throws SQLExceptionWrap in your RepositoryException(msg, cause)Hides the technology, keeps the cause
“Something went wrong” for everythingNot a custom AppException aloneA single generic type tells callers nothing

Always offer a (message, cause) constructor. If your exception only has (String message), anyone wrapping a lower-level exception in it is forced to throw the original away, and the “Caused by:” part of the stack trace disappears.

4 Step by step

Building InsufficientFundsException

  1. Pick the parent and a clear name

    class InsufficientFundsException extends Exception {
    Extending Exception makes it checked. Because Throwable implements Serializable, so does your class; javac -Xlint:serial warns that it has no serialVersionUID. Add private static final long serialVersionUID = 1L; if the exception may travel between JVMs.
  2. Add the data as final fields

    private final long shortfall;
    The field lives inside the exception object on the heap, next to the fields inherited from Throwable (message, cause, stack trace). final means it can't change after the object is built.
  3. Write the constructors

    public InsufficientFundsException(String message, long shortfall) {
        super(message);
        this.shortfall = shortfall;
    }
    super(message) runs Exception's constructor first, which stores the message and calls fillInStackTrace(). The recorded trace starts at the line with new; the constructor frames themselves are left out.
  4. Add getters (no setters)

    public long getShortfall() { return shortfall; }
    Callers read the value with a normal method call on the caught object. getMessage(), getCause() and toString() are inherited.
  5. Throw it and catch it by type

    throw new InsufficientFundsException("Need Rs " + amount + ", have Rs " + balance, amount - balance);
    …
    catch (InsufficientFundsException e) { … e.getShortfall() … }
    To the JVM it's an ordinary exception: same handler search, same frame popping. The exception table simply names your class, so only a catch for it (or a parent) matches.
5 See it happen

A custom exception is born, thrown and read: both views

Left: the code and the console. Right: the stack, and the exception object with its inherited and its own fields.

Withdrawing Rs 5,000 from a balance of Rs 2,500 6 steps
What you see withdraw(5000); // balance 2500 new InsufficientFundsException(…) super("Short by Rs 2500"); this.shortfall = shortfall; throw …; → catch (Insufficient… e) e.getShortfall() // nothing printed yet Short by Rs 2500 [ Add Rs 2500 ] button shown The rule check fails inside withdraw. new runs your constructor. The parent stores the message and trace. Your own field is filled in. Thrown and caught by its exact type. The app uses the number directly. myphatshala.in
Behind the screen Stack Heap new Insufficient… withdraw(5000) main(args) main e → #1 withdraw popped #1 InsufficientFundsException inherited from Throwable: message = null message = "Short by Rs 2500" stackTrace = (empty) stackTrace = withdraw, main declared in your class: shortfall = 0 shortfall = 2500 withdraw is running; no exception yet. Object allocated: fields at default values. Throwable's part filled by super(message). Your field set; constructor frame popped next. withdraw popped; main's e refers to #1. getShortfall() reads the field: 2500. myphatshala.in
6 Real-world use

Custom exceptions in real systems

Most large Java applications define a small set of business exceptions. Their names and fields become part of the application's vocabulary.

Watch it with an example: a delivery app that offers pickup instead

A food-delivery backend can't deliver to some PIN codes. Its custom exception carries the PIN and the nearest hub, so the app can offer pickup without reading any message text.

NotServiceableException carries the data the app needs 6 steps
Animated example placeOrder("Biryani", "400099"); if (!served(pin)) throw new NotServ… super(msg); this.nearestHub = hub; catch (NotServiceableException e) e.getNearestHub() // "Andheri" app.offerPickup(e.getNearestHub()); CustomerPIN 400099 Order serviceserved: 4000xx? #1 NotServiceableException message: "Can't deliver to 400099" pincode = "400099" nearestHub = "Andheri" throw! Delivery not available Pick up from Andheri hub? Yes, pickup The customer orders to PIN 400099. That PIN isn't served: the service throws. The object carries the PIN and the nearest hub. The app's catch picks it out by its type. A getter gives the hub; no text parsing. Result: a useful pickup offer, not just an error. myphatshala.in
402short: 2500

1. Mapping to API responses

A REST API maps each business exception to a status and a JSON body: InsufficientFundsException → a clear error code and the shortfall, OutOfStockException → the available quantity. One central handler does the mapping.

Exception type + fields→@ExceptionHandler→JSON error response

2. Exception families per module

A payments module defines PaymentException with subclasses (declined, timeout, limit). Callers can catch the whole family or one member, and new members don't break old catch blocks.

PaymentException→Declined / Timeout / Limit→Catch broad or narrow
code=QTY_LIMITcount=1,204 today

3. Error codes for dashboards and support

An errorCode field (like QTY_LIMIT or KYC_PENDING) lets monitoring count failures by kind and lets support staff look up a fix, whatever the message wording.

errorCode field→log + metrics→Dashboards, help articles
SQLRepo

4. Hiding technology behind your own type

A repository catches the database driver's exceptions and throws RepositoryException(msg, cause). Service code no longer depends on which database is used, and the cause keeps the detail.

SQLException→new RepositoryException(msg, e)→Service stays clean

Examples are simplified. Frameworks such as Spring provide the central handler; the exception design (names, fields, causes) is up to your team.

7 Run the code

The wallet, with a proper InsufficientFundsException

class InsufficientFundsException extends Exception {
    private final long shortfall;

    public InsufficientFundsException(String message, long shortfall) {
        super(message);
        this.shortfall = shortfall;
    }

    public long getShortfall() {
        return shortfall;
    }
}

public class Main {
    static long balance = 3000;

    static void withdraw(long amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(
                "Need Rs " + amount + ", have Rs " + balance, amount - balance);
        }
        balance -= amount;
    }

    public static void main(String[] args) {
        long[] requests = {1000, 5000, 2000};
        for (long r : requests) {
            try {
                withdraw(r);
                System.out.println("Withdrew Rs " + r + ", balance Rs " + balance);
            } catch (InsufficientFundsException e) {
                System.out.println(e.getMessage() + " -> add Rs " + e.getShortfall());
            }
        }
    }
}
Withdrew Rs 1000, balance Rs 2000 Need Rs 5000, have Rs 2000 -> add Rs 3000 Withdrew Rs 2000, balance Rs 0
  • extends ExceptionChecked: withdraw must declare it and main must catch it.
  • super(message)Gives the message to Exception, so getMessage() and the stack trace work.
  • private final long shortfallData for code; set once in the constructor, read with a getter.
  • e.getShortfall()The catch block uses the number directly, whatever the message says.
8 Line by line

Your constructor runs, then the exception travels

Watch a frame for CouponExpiredException's constructor appear on top of the stack while the object is built, before the throw pops apply.

Worked examples: real problems, solved and traced in memory

Three business exceptions designed from scratch. Run each solution, then step through it: you will see the custom exception's own constructor frame being pushed when it is created, then the frames popped as it travels to the catch block.

Example 1Banking · savings account

InsufficientFundsException with a shortfall

Problem statement

A transfer withdraws from the sender's account. If the balance is too low, withdraw throws a checked InsufficientFundsException that carries the shortfall, so the app can say exactly how much to add.

Input
balance = Rs 4,000
transfer Rs 6,500
Expected output
Transfer failed: Short by Rs 2500
Add Rs 2500 to continue

Approach

  1. Define InsufficientFundsException extends Exception with a shortfall field, set in the constructor.
  2. withdraw declares throws InsufficientFundsException; so does transfer, which doesn't catch it.
  3. main catches it and reads getShortfall() to build the advice.

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 2E-commerce · checkout

An unchecked InvalidOrderException with an error code

Problem statement

The checkout validates each order line. Problems are reported with an unchecked InvalidOrderException that holds a short error code (for the app and analytics) as well as a message (for people).

Input
lines: qty 0, qty 15 (limit is 10 per item)
Expected output
qty 0: [QTY_ZERO] Quantity cannot be 0
qty 15: [QTY_LIMIT] Max 10 per item, got 15

Approach

  1. InvalidOrderException extends RuntimeException: no throws clauses needed anywhere.
  2. checkLine throws it with codes QTY_ZERO or QTY_LIMIT.
  3. main loops over lines and uses getCode() in its 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 3Fintech · UPI payments

PaymentFailedException that keeps the cause

Problem statement

A payment service talks to a bank API. A low-level failure (here an IllegalStateException from the API client) is wrapped in your own PaymentFailedException with the transaction id in the message and the original as the cause.

Input
transaction id = "UPI7731", bank API is down
Expected output
Payment UPI7731 failed
Technical cause: Bank API unavailable

Approach

  1. PaymentFailedException(String message, Throwable cause) passes both to super.
  2. pay catches the low-level exception and throws the business one, with context only it knows.
  3. main sees one business exception type, and can still reach the technical cause.

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.5 in one screen

  • Custom exception = a class extending Exception (checked) or RuntimeException (unchecked).
  • Constructors call super(message) / super(message, cause); constructors aren't inherited.
  • Name it after what happened, ending in Exception.
  • Carry data in private final fields with getters; build the message from them.
  • Group related exceptions under a base class; catch subclasses first.
  • Use built-ins (IllegalArgumentException, IllegalStateException) when they already say it.
  • The stack trace starts at the line with new; your class name appears in it.

Interview and viva questions

How do you create a custom exception in Java?

Write a class that extends Exception (checked) or RuntimeException (unchecked), add constructors that pass the message and optionally a cause to super, add any fields the caller needs, and throw it with throw new YourException(...).

Should a custom exception be checked or unchecked?

Checked if every caller must consciously handle the situation and can recover (for example insufficient funds in a banking API). Unchecked if it represents a rule violation usually handled at one boundary, or if it must pass through lambdas. Many modern codebases default to unchecked and document the exceptions well.

Why make exception fields final?

An exception describes something that already happened. It is passed around, logged and maybe inspected by several handlers; if it could be modified, one handler could change what another sees. Final fields with getters keep it immutable.

When should you NOT create a custom exception?

When a standard one already expresses the problem (bad argument, wrong state, unsupported operation), when the caller would never handle it differently from other errors, or when you'd create one class per message. Too many exception types make code harder to read, not easier.

What is serialVersionUID and do exceptions need it?

Throwable implements Serializable, so every exception class is serializable. serialVersionUID is a version number used when objects are serialized, for example sent between JVMs. javac warns about it only with -Xlint:serial; add private static final long serialVersionUID = 1L; if your exceptions cross process boundaries.

Practice problems

Practise designing your own exception classes: choosing the parent, writing the constructors, adding useful fields, and catching them by type. The last four are real business tasks.

P1Easy

Your first custom exception

Create an unchecked InvalidAgeException and throw it from checkVoterAge when the age is below 18.

Input
ages = 21, 16
Expected output
Age 21 is eligible
InvalidAgeException: Voter must be 18+, got 16
Show solution and explanation
class InvalidAgeException extends RuntimeException {
    public InvalidAgeException(String message) {
        super(message);
    }
}

public class Main {
    static void checkVoterAge(int age) {
        if (age < 18) {
            throw new InvalidAgeException("Voter must be 18+, got " + age);
        }
        System.out.println("Age " + age + " is eligible");
    }

    public static void main(String[] args) {
        try {
            checkVoterAge(21);
            checkVoterAge(16);
        } catch (InvalidAgeException e) {
            System.out.println(e);
        }
    }
}
Age 21 is eligible InvalidAgeException: Voter must be 18+, got 16

How it works: a custom exception is just a class that extends an exception class. The constructor passes the message to the parent with super(message), which is where getMessage() and toString() get it from. Because it extends RuntimeException, no throws clause is needed.

P2Easy

A checked custom exception

Create a checked SeatUnavailableException (extends Exception). bookSeat must declare it, and main must catch it.

Input
booked seats: 12; requests: 7, 12
Expected output
Seat 7 booked
Sorry: Seat 12 is already booked
Show solution and explanation
class SeatUnavailableException extends Exception {
    public SeatUnavailableException(String message) {
        super(message);
    }
}

public class Main {
    static int bookedSeat = 12;

    static void bookSeat(int seat) throws SeatUnavailableException {
        if (seat == bookedSeat) {
            throw new SeatUnavailableException("Seat " + seat + " is already booked");
        }
        System.out.println("Seat " + seat + " booked");
    }

    public static void main(String[] args) {
        int[] requests = {7, 12};
        for (int s : requests) {
            try {
                bookSeat(s);
            } catch (SeatUnavailableException e) {
                System.out.println("Sorry: " + e.getMessage());
            }
        }
    }
}
Seat 7 booked Sorry: Seat 12 is already booked

How it works: extending Exception (not RuntimeException) makes it checked, so the compiler forces every caller of bookSeat to catch it or declare it. Choose this when you want callers to be unable to forget the situation.

P3Easy

The four standard constructors

Give ReportException the four constructors that the JDK's own exceptions have: none, message, message + cause, cause. Create one of each and print its message and cause.

Input
cause = IOException("disk full")
Expected output
message=null | cause=null
message=Report failed | cause=null
message=Report failed | cause=java.io.IOException: disk full
message=java.io.IOException: disk full | cause=java.io.IOException: disk full
Show solution and explanation
import java.io.IOException;

class ReportException extends Exception {
    public ReportException() { super(); }
    public ReportException(String message) { super(message); }
    public ReportException(String message, Throwable cause) { super(message, cause); }
    public ReportException(Throwable cause) { super(cause); }
}

public class Main {
    public static void main(String[] args) {
        IOException io = new IOException("disk full");
        ReportException[] all = {
            new ReportException(),
            new ReportException("Report failed"),
            new ReportException("Report failed", io),
            new ReportException(io)
        };
        for (ReportException r : all) {
            System.out.println("message=" + r.getMessage() + " | cause=" + r.getCause());
        }
    }
}
message=null | cause=null message=Report failed | cause=null message=Report failed | cause=java.io.IOException: disk full message=java.io.IOException: disk full | cause=java.io.IOException: disk full

How it works: each constructor just passes its arguments to the matching constructor of Exception. Note the last one: with only a cause, the message becomes cause.toString(). The message + cause constructor is the most important one; without it, people wrapping exceptions in yours would lose the original.

P4Medium

A family of exceptions

Create a base ShopException and two subclasses, OutOfStockException and CouponExpiredException. Show that a single catch (ShopException e) handles both, and that a more specific catch placed first handles its own type.

Input
problems thrown: out of stock, coupon expired
Expected output
Suggest similar items: Kurta size M is out of stock
CouponExpiredException: DIWALI20 expired on 05-11
Show solution and explanation
class ShopException extends RuntimeException {
    public ShopException(String message) { super(message); }
}

class OutOfStockException extends ShopException {
    public OutOfStockException(String message) { super(message); }
}

class CouponExpiredException extends ShopException {
    public CouponExpiredException(String message) { super(message); }
}

public class Main {
    static void process(int problem) {
        if (problem == 1) throw new OutOfStockException("Kurta size M is out of stock");
        if (problem == 2) throw new CouponExpiredException("DIWALI20 expired on 05-11");
    }

    public static void main(String[] args) {
        for (int p = 1; p <= 2; p++) {
            try {
                process(p);
            } catch (OutOfStockException e) {
                System.out.println("Suggest similar items: " + e.getMessage());
            } catch (ShopException e) {
                System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
            }
        }
    }
}
Suggest similar items: Kurta size M is out of stock CouponExpiredException: DIWALI20 expired on 05-11

How it works: a common base class lets callers choose: catch the whole family in one place, or pick out one member for special handling. As always, the more specific catch must come first. Large applications often have one base exception per module.

P5Medium

An exception that carries data

An OTP is valid for 120 seconds. Create OtpExpiredException with a field secondsLate and a getter, so the caller can decide what to show without parsing the message.

Input
OTP entered after 95 s, then after 160 s
Expected output
OTP accepted after 95 s
OTP expired 40 s ago
Tip: tap 'Resend OTP'
Show solution and explanation
class OtpExpiredException extends RuntimeException {
    private final int secondsLate;

    public OtpExpiredException(int secondsLate) {
        super("OTP expired " + secondsLate + " s ago");
        this.secondsLate = secondsLate;
    }

    public int getSecondsLate() {
        return secondsLate;
    }
}

public class Main {
    static final int VALID_FOR = 120;

    static void verify(int secondsTaken) {
        if (secondsTaken > VALID_FOR) {
            throw new OtpExpiredException(secondsTaken - VALID_FOR);
        }
        System.out.println("OTP accepted after " + secondsTaken + " s");
    }

    public static void main(String[] args) {
        int[] attempts = {95, 160};
        for (int t : attempts) {
            try {
                verify(t);
            } catch (OtpExpiredException e) {
                System.out.println(e.getMessage());
                if (e.getSecondsLate() < 60) {
                    System.out.println("Tip: tap 'Resend OTP'");
                }
            }
        }
    }
}
OTP accepted after 95 s OTP expired 40 s ago Tip: tap 'Resend OTP'

How it works: the message is for humans and logs; the field is for code. Making it final with only a getter keeps the exception immutable, which matters because exception objects are passed around and logged. The constructor builds the message from the data, so the two never disagree.

P6Medium

Build the message from fields

Instead of building the message at every throw, let LimitExceededException build it in its constructor from the limit and the attempted value.

Input
UPI daily limit Rs 1,00,000; attempts Rs 40,000 and Rs 1,25,000
Expected output
Paid Rs 40000
Limit Rs 100000 exceeded by Rs 25000
Split into 2 payments of Rs 62500
Show solution and explanation
class LimitExceededException extends RuntimeException {
    private final long limit;
    private final long attempted;

    public LimitExceededException(long limit, long attempted) {
        super("Limit Rs " + limit + " exceeded by Rs " + (attempted - limit));
        this.limit = limit;
        this.attempted = attempted;
    }

    public long getLimit() { return limit; }
    public long getAttempted() { return attempted; }
}

public class Main {
    static void pay(long amount) {
        long limit = 100000;
        if (amount > limit) {
            throw new LimitExceededException(limit, amount);
        }
        System.out.println("Paid Rs " + amount);
    }

    public static void main(String[] args) {
        long[] payments = {40000, 125000};
        for (long p : payments) {
            try {
                pay(p);
            } catch (LimitExceededException e) {
                System.out.println(e.getMessage());
                System.out.println("Split into 2 payments of Rs " + e.getAttempted() / 2);
            }
        }
    }
}
Paid Rs 40000 Limit Rs 100000 exceeded by Rs 25000 Split into 2 payments of Rs 62500

How it works: every throw site now writes only new LimitExceededException(limit, amount), and the wording stays consistent everywhere. The caller still gets the raw numbers through getters to compute a suggestion.

P7Medium

Don't lose the cause

Wrap a low-level NumberFormatException in your own InvalidPriceException using a (message, cause) constructor. Print both the message and the cause.

Input
price text = "12,50"
Expected output
Price "12,50" is not valid
Caused by: java.lang.NumberFormatException: For input string: "12,50"
Show solution and explanation
class InvalidPriceException extends RuntimeException {
    public InvalidPriceException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class Main {
    static double parsePrice(String text) {
        try {
            return Double.parseDouble(text);
        } catch (NumberFormatException e) {
            throw new InvalidPriceException("Price \"" + text + "\" is not valid", e);
        }
    }

    public static void main(String[] args) {
        try {
            parsePrice("12,50");
        } catch (InvalidPriceException e) {
            System.out.println(e.getMessage());
            System.out.println("Caused by: " + e.getCause());
        }
    }
}
Price "12,50" is not valid Caused by: java.lang.NumberFormatException: For input string: "12,50"

How it works: your exception speaks the language of your application (“price is not valid”), while the cause keeps the technical detail. A custom exception without a cause-accepting constructor forces people to throw the original away, a common review comment.

P8Business problem

Withdrawals with InsufficientFundsException

Process a day's withdrawal requests on a savings account. A request larger than the balance throws a checked InsufficientFundsException carrying the shortfall. Print each result and the total amount refused.

Input
opening balance Rs 10,000; requests 2500, 9000, 4000, 3500
Expected output
Rs 2500 paid, balance Rs 7500
Rs 9000 refused (Insufficient funds: short by Rs 1500)
Rs 4000 paid, balance Rs 3500
Rs 3500 paid, balance Rs 0
Total refused today: Rs 9000
Show solution and explanation
class InsufficientFundsException extends Exception {
    private final long shortfall;

    public InsufficientFundsException(long shortfall) {
        super("Insufficient funds: short by Rs " + shortfall);
        this.shortfall = shortfall;
    }

    public long getShortfall() {
        return shortfall;
    }
}

public class Main {
    static long balance = 10000;

    static void withdraw(long amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(amount - balance);
        }
        balance -= amount;
    }

    public static void main(String[] args) {
        long[] requests = {2500, 9000, 4000, 3500};
        long refused = 0;
        for (long r : requests) {
            try {
                withdraw(r);
                System.out.println("Rs " + r + " paid, balance Rs " + balance);
            } catch (InsufficientFundsException e) {
                refused += r;
                System.out.println("Rs " + r + " refused (" + e.getMessage() + ")");
            }
        }
        System.out.println("Total refused today: Rs " + refused);
    }
}
Rs 2500 paid, balance Rs 7500 Rs 9000 refused (Insufficient funds: short by Rs 1500) Rs 4000 paid, balance Rs 3500 Rs 3500 paid, balance Rs 0 Total refused today: Rs 9000

How it works:

  1. Insufficient funds is a normal business outcome every caller must handle, so a checked exception makes it impossible to forget.
  2. The check happens before the balance changes, so a refused request leaves the account untouched.
  3. The shortfall travels as data in the exception, ready for a message like “Add Rs 1,500 to continue”.
P9Business problem

Out-of-stock items in an order

An order has several lines. reserve throws OutOfStockException with the item name and the units available. Fulfil what you can, and for each failed line print a suggestion to order only the available units.

Input
stock: Rice 10, Dal 3, Oil 0
order: Rice x4, Dal x5, Oil x2
Expected output
Rice x4 reserved
Problem: Dal: requested 5, available 3
  Suggest: Dal x3
Problem: Oil: requested 2, available 0
  Suggest: remove Oil
Show solution and explanation
class OutOfStockException extends RuntimeException {
    private final String item;
    private final int available;

    public OutOfStockException(String item, int requested, int available) {
        super(item + ": requested " + requested + ", available " + available);
        this.item = item;
        this.available = available;
    }

    public String getItem() { return item; }
    public int getAvailable() { return available; }
}

public class Main {
    static String[] items = {"Rice", "Dal", "Oil"};
    static int[] stock = {10, 3, 0};

    static void reserve(int index, int qty) {
        if (qty > stock[index]) {
            throw new OutOfStockException(items[index], qty, stock[index]);
        }
        stock[index] -= qty;
    }

    public static void main(String[] args) {
        int[] orderQty = {4, 5, 2};
        for (int i = 0; i < items.length; i++) {
            try {
                reserve(i, orderQty[i]);
                System.out.println(items[i] + " x" + orderQty[i] + " reserved");
            } catch (OutOfStockException e) {
                System.out.println("Problem: " + e.getMessage());
                if (e.getAvailable() > 0) {
                    System.out.println("  Suggest: " + e.getItem() + " x" + e.getAvailable());
                } else {
                    System.out.println("  Suggest: remove " + e.getItem());
                }
            }
        }
    }
}
Rice x4 reserved Problem: Dal: requested 5, available 3 Suggest: Dal x3 Problem: Oil: requested 2, available 0 Suggest: remove Oil

How it works:

  1. The exception carries exactly what the caller needs (item and available units), so the suggestion logic doesn't have to look anything up again.
  2. It is unchecked: an order service usually handles it at one boundary, and the type name alone tells you what happened.
  3. Each line is reserved independently, so one missing item doesn't cancel the whole order.
P10Business problem

Retry only what is retryable

A payment gateway can fail in two ways: a timeout (worth retrying) or a declined card (never retry). Model this with PaymentFailedException that has a retryable flag, and retry up to 3 times only when it is true.

Input
payment 1: times out twice, then succeeds
payment 2: card declined
Expected output
Payment 1:
  Attempt 1: Gateway timeout
  Attempt 2: Gateway timeout
  Paid on attempt 3
Payment 2:
  Attempt 1: Card declined by bank
  Not retrying
Show solution and explanation
class PaymentFailedException extends RuntimeException {
    private final boolean retryable;

    public PaymentFailedException(String message, boolean retryable) {
        super(message);
        this.retryable = retryable;
    }

    public boolean isRetryable() {
        return retryable;
    }
}

public class Main {
    static int timeoutsLeft = 2;

    static void charge(String card) {
        if (card.equals("BLOCKED")) {
            throw new PaymentFailedException("Card declined by bank", false);
        }
        if (timeoutsLeft > 0) {
            timeoutsLeft--;
            throw new PaymentFailedException("Gateway timeout", true);
        }
    }

    static void pay(String card) {
        for (int attempt = 1; attempt <= 3; attempt++) {
            try {
                charge(card);
                System.out.println("  Paid on attempt " + attempt);
                return;
            } catch (PaymentFailedException e) {
                System.out.println("  Attempt " + attempt + ": " + e.getMessage());
                if (!e.isRetryable()) {
                    System.out.println("  Not retrying");
                    return;
                }
            }
        }
        System.out.println("  Gave up after 3 attempts");
    }

    public static void main(String[] args) {
        System.out.println("Payment 1:");
        pay("4111-XXXX");
        System.out.println("Payment 2:");
        pay("BLOCKED");
    }
}
Payment 1: Attempt 1: Gateway timeout Attempt 2: Gateway timeout Paid on attempt 3 Payment 2: Attempt 1: Card declined by bank Not retrying

How it works:

  1. A field on the exception lets the thrower, who knows the reason, tell the caller how to react.
  2. Retrying a declined card would only annoy the bank and the customer; retrying a timeout often works.
  3. The alternative, two separate classes (RetryablePaymentException, PaymentDeclinedException), is also common; both designs keep the decision out of message text.
P11Business problem

Config file errors that name the key

Load settings from key=value lines. Any problem must surface as one ConfigException that knows which key failed and keeps the original cause, so the operator knows exactly what to fix.

Input
lines = {"maxUsers=500", "timeoutSec=thirty", "region=ap-south"}
Expected output
Config key 'timeoutSec': expected a whole number, got thirty
Fix key: timeoutSec
Cause: NumberFormatException
Show solution and explanation
class ConfigException extends RuntimeException {
    private final String key;

    public ConfigException(String key, String message, Throwable cause) {
        super("Config key '" + key + "': " + message, cause);
        this.key = key;
    }

    public String getKey() {
        return key;
    }
}

public class Main {
    static int readInt(String line) {
        String[] kv = line.split("=");
        try {
            return Integer.parseInt(kv[1]);
        } catch (NumberFormatException e) {
            throw new ConfigException(kv[0], "expected a whole number, got " + kv[1], e);
        }
    }

    public static void main(String[] args) {
        String[] lines = {"maxUsers=500", "timeoutSec=thirty", "region=ap-south"};
        try {
            int maxUsers = readInt(lines[0]);
            int timeout = readInt(lines[1]);
            System.out.println("Loaded: " + maxUsers + ", " + timeout);
        } catch (ConfigException e) {
            System.out.println(e.getMessage());
            System.out.println("Fix key: " + e.getKey());
            System.out.println("Cause: " + e.getCause().getClass().getSimpleName());
        }
    }
}
Config key 'timeoutSec': expected a whole number, got thirty Fix key: timeoutSec Cause: NumberFormatException

How it works:

  1. A low-level NumberFormatException doesn't say which setting was wrong; the custom exception adds that context and keeps the cause.
  2. The start-up code catches one type, whatever went wrong inside.
  3. The key is a field, so a tool could highlight the bad line in the file automatically.

Real-world scenario questions (asked in company interviews)

Exception design questions test whether you think about the people and code that will catch your exceptions. Decide your answer first, then open the model answer.

Fintech · wallet APIFresher

The button that parsed a message

The mobile app reads the shortfall by cutting the last word out of an exception message. After a wording change, the “Add money” button shows “2500.” with a dot and fails. How would you redesign the backend's exception?

The interviewer is checking: do you separate data for code from text for people?

Show a strong answer
  • Create InsufficientFundsException with a private final long shortfall field (in paise or as BigDecimal for money) and a getter.
  • Build the message from the field in the constructor so text and data always agree.
  • Have the API's error handler put the field into the JSON response ("shortfall": 2500), so the app never parses text.

Avoid: treating message strings as an API. They are for logs and humans.

E-commerce · code review1–3 years

Forty exception classes

A pull request adds 40 new exception classes like NameTooLongException, NameEmptyException, PinTooShortException, each used in one place and all handled identically by showing the message. What feedback do you give?

The interviewer is checking: do you know when a custom exception adds value?

Show a strong answer
  • A new type is worth it only if some caller handles it differently or needs its data. Here every handler does the same thing.
  • Replace them with one ValidationException carrying a field name and an error code (or simply IllegalArgumentException with good messages).
  • Keep separate types for genuinely different business outcomes, such as OutOfStock or PaymentDeclined.

Avoid: one class per message. It bloats the codebase without helping callers.

Banking · integration1–3 years

Checked or unchecked for a core-banking client?

You are writing a client library other teams will use to call the core banking system. It can fail with insufficient funds, account frozen, or the core system being down. Design the exception types.

The interviewer is checking: can you design a small, useful exception hierarchy and justify checked vs unchecked?

Show a strong answer
  • A base CoreBankingException so callers can catch everything from this library in one place.
  • Subclasses: InsufficientFundsException (with shortfall), AccountFrozenException (with reason code), CoreUnavailableException (with a retryable flag, wrapping the network cause).
  • Business outcomes could be checked so callers can't ignore them; the outage could be unchecked or checked depending on team convention. State the choice and document it.
  • Every class gets a (message, cause) constructor.

Avoid: exposing raw low-level exceptions (sockets, JSON parsing) to callers.

SaaS · production debugging3+ years

Custom exception that hid the real error

Logs show ReportGenerationException: Report failed thousands of times, with traces ending inside your service. The team can't tell whether the database, the template or the PDF library is failing. The exception class has only a (String message) constructor. What do you change?

The interviewer is checking: do you know how custom exceptions should preserve causes and context?

Show a strong answer
  • Add a (String message, Throwable cause) constructor and use it at every wrap site, so the trace shows “Caused by:” with the real failure.
  • Add context to the message or fields: report id, tenant, stage (query, template, render).
  • Optionally add a stage or errorCode field so dashboards can count failures by stage.

Avoid: new ReportGenerationException("Report failed: " + e.getMessage()). It loses the type and the stack trace of the real problem.