Think of a custom exception as a bright warning label you add to your own code when Java's built-in errors are too vague. Interviewers love this topic because it shows whether you can turn a business rule into a clear, maintainable failure.
Question: What is a custom exception in Java, and when should I create one?
Answer: A custom exception is a class you write to represent a specific problem in your application, like InsufficientBalanceException or InvalidOrderStateException. In Java, it usually extends Exception for a checked exception or RuntimeException for an unchecked one. You create it when a built-in exception does not describe the real business problem clearly enough.
Interview-Ready Answer: I use a custom exception when I want a failure to mean something specific in my domain, not just a generic Java error. I usually extend Exception if the caller must handle it, or RuntimeException if it is a programming or validation error. I also add constructors for a message and cause, so I can keep the original root problem and make debugging easier.
A custom exception is just a class whose job is to say, this exact thing went wrong. The value is not fancy syntax; the value is meaning. Instead of throwing a generic Exception, you give the caller a name that matches the business rule, so code becomes easier to read, test, and log.
Exception or RuntimeException.new and immediately throw it.throws or handle it with try/catch.Choose the type based on who should deal with the problem. If the caller can reasonably recover, a checked exception is useful. If the issue is a programming mistake, bad input, or a rule violation that should fail fast, an unchecked exception is often cleaner.
| Choice | Extends | Compiler forces handling? | Best for | Typical example |
|---|---|---|---|---|
| Checked | Exception | Yes | Recoverable flows | InsufficientBalanceException |
| Unchecked | RuntimeException | No | Programming or validation errors | InvalidStateException |
super(message, cause) when wrapping.Throwing an exception is much slower than a normal if check because the JVM has to allocate the object and capture a stack trace. In a web app, that stack trace can include tens of frames, so the cost grows with call depth. That is why exceptions belong on rare failure paths, not inside hot loops or tight data-processing code.
One practical detail: custom exceptions are objects, so they can carry fields, but keep them small and meaningful. If the exception may be serialized across a remote boundary, define a serialVersionUID to keep versions compatible. And when you translate an exception from a lower layer, always preserve the root cause so you do not lose the real bug.
Memory image: a custom exception is a labeled smoke alarm: the room is still on fire, but now you know exactly which room and why.
Imagine a checkout service in an e-commerce app. When a customer places an order, the service checks stock, payment status, and shipping rules. A custom exception like InsufficientStockException or PaymentDeclinedException makes the failure explicit, so the API can return a helpful message and the monitoring system can count the right kind of problem.
Now imagine a misunderstanding: a developer catches every failure and rethrows a generic RuntimeException. In production, the logs start showing the same vague message for stock shortages, card declines, and database timeouts. Users see a bland 500 Internal Server Error, support cannot tell what failed, and the retry system may keep retrying a payment that will never succeed. The symptom is a spike in failed checkouts, noisy logs with repeated generic stack traces, and a dashboard that cannot separate business failures from real system outages. A well-named custom exception prevents that confusion and makes the incident easier to diagnose.
import java.math.BigDecimal;
import java.util.Objects;
public class CustomExceptionDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount("A-1001", new BigDecimal("100.00"));
System.out.println("Starting balance: " + account.getBalance());
try {
account.withdraw(new BigDecimal("30.00"));
System.out.println("After first withdrawal: " + account.getBalance());
} catch (InsufficientBalanceException e) {
System.out.println("Unexpected failure: " + e.getMessage());
}
try {
account.withdraw(new BigDecimal("80.00"));
System.out.println("This line will not run.");
} catch (InsufficientBalanceException e) {
// The custom type tells us exactly what business rule failed.
System.out.println("Custom exception caught: " + e.getMessage());
}
try {
account.withdraw(new BigDecimal("-5.00"));
} catch (IllegalArgumentException e) {
// Validation errors are often better as unchecked exceptions.
System.out.println("Bad input: " + e.getMessage());
}
}
static class BankAccount {
private final String accountId;
private BigDecimal balance;
BankAccount(String accountId, BigDecimal openingBalance) {
this.accountId = Objects.requireNonNull(accountId, "accountId");
this.balance = Objects.requireNonNull(openingBalance, "openingBalance");
}
void withdraw(BigDecimal amount) throws InsufficientBalanceException {
Objects.requireNonNull(amount, "amount");
if (amount.signum() <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if (balance.compareTo(amount) < 0) {
// This is the domain-specific failure we want callers to recognize.
throw new InsufficientBalanceException(
"Account " + accountId + " has only " + balance + ", but needs " + amount
);
}
balance = balance.subtract(amount);
}
BigDecimal getBalance() {
return balance;
}
}
static class InsufficientBalanceException extends Exception {
private static final long serialVersionUID = 1L;
InsufficientBalanceException(String message) {
super(message);
}
InsufficientBalanceException(String message, Throwable cause) {
super(message, cause);
}
}
}
Follow-up & Tricky Questions:
Exception everywhere? Because a generic type hides the real meaning of the failure and makes catch blocks less precise. A specific exception improves readability, logging, and testing.Throwable cause and pass it to super(message, cause). That keeps the root stack trace visible when you debug.serializable? Java exceptions already implement Serializable, but if the type may cross JVM boundaries or be stored long-term, define serialVersionUID for compatibility.throw the same as throws? No. throw creates and sends one exception object, while throws declares that a method may pass an exception to its caller.Tricky / gotcha questions:
RuntimeException, do I lose stack traces? No. You still get a stack trace when the exception is thrown; it is just unchecked, so the compiler does not force a catch or declaration.IllegalArgumentException is often enough and keeps the API simpler.Common Mistakes:
(message, cause) when wrapping another exception.if checks for expected branches and exceptions only for rare failure paths.Memory Hook: A custom exception is a labeled smoke alarm: the alarm says something is wrong, and your label tells you exactly which room is burning.
Cheat Sheet:
Exception for checked, RuntimeException for unchecked.Practice Tasks:
InvalidAgeException and throw it when age is below 18.RuntimeException and remove the throws clause.