Java Custom Exceptions

In the last lesson you learned about the Java throws keyword. Java’s built-in exceptions are general-purpose, so a plain IllegalArgumentException for an overdraft does not say much. Java lets you create your own exception types that describe your program’s specific problems. These are called custom exceptions.

πŸ€” Why create your own exceptions?

Think about an error message on a label. A box marked β€œProblem” tells you almost nothing. A box marked β€œNot enough money in the account” tells you exactly what happened and what to do. A custom exception is that second, clearer label.

Imagine a withdraw method that fails when the balance is too low. With a built-in exception, the error is vague.

if (amount > balance) {
throw new IllegalArgumentException("Not enough money"); // ❌ generic type
}

This works, but IllegalArgumentException is thrown all over the place for all sorts of bad arguments. So a caller cannot easily catch just the not-enough-money case. A domain-specific exception, one named after a problem in your own business area like money, age, or orders, fixes this:

  • The name explains the problem on its own. InsufficientFundsException reads like a sentence.
  • Callers can catch exactly that problem with catch (InsufficientFundsException e) and ignore unrelated errors.
  • Different problems get different handling. Catch InsufficientFundsException one way and AccountFrozenException another.
  • Your code becomes self-documenting. The error types list everything that can go wrong.

A good name does the teaching for you. InvalidAgeException, OrderNotFoundException, PasswordTooWeakException β€” each tells the story before you read a line inside.

🧩 How to create a custom exception

A custom exception is just a class that extends an existing exception class. You pick one of two parents, and the choice decides checked or unchecked:

  • Extend Exception to make a checked exception. Callers must handle it or declare it.
  • Extend RuntimeException to make an unchecked exception. Callers are not forced to handle it.

Here is the simplest checked custom exception. Read the code first, then we walk through it.

class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message); // βœ… pass the message up to the Exception class
}
}

Now line by line. The header says extends Exception, so this is a checked exception. The constructor takes a String message, the description of what went wrong. Inside, super(message) hands that message to the parent Exception constructor. That one call is what makes getMessage() return your text later. And that is a complete, working custom exception.

It is good practice to add more than one constructor, so your exception is flexible. The standard set looks like this.

class InsufficientFundsException extends Exception {
public InsufficientFundsException() {
super(); // βœ… no message
}
public InsufficientFundsException(String message) {
super(message); // βœ… message only
}
public InsufficientFundsException(String message, Throwable cause) {
super(message, cause); // βœ… message plus the original error that caused this
}
}

Why the third constructor? Sometimes one error triggers another. A database lookup fails, and because of that you cannot check the balance. The cause keeps the original error attached, so the full chain shows up in the stack trace. You do not need all three every time. A single message constructor is fine for most cases.

🏷️ Adding extra fields to your exception

A custom exception can carry data, not just a message. When a withdrawal fails, the caller might want to know how much the account was short. A built-in exception cannot tell you that. Your own one can, by adding a field and a getter.

Here we store the shortfall amount so the caller can read it later.

class InsufficientFundsException extends Exception {
private final double shortfall; // βœ… extra data the caller might want
public InsufficientFundsException(String message, double shortfall) {
super(message);
this.shortfall = shortfall;
}
public double getShortfall() { // βœ… getter so callers can read it
return shortfall;
}
}

We added a shortfall field, set it in the constructor, and exposed it with getShortfall(). Now the catch block can read the exact amount the account was missing and, say, offer to transfer that much from savings. The message is for humans. The field is for code. Both travel together in the same exception object.

πŸ’‘ Throwing and catching a custom exception

You throw and catch a custom exception exactly like a built-in one, no special syntax. Let’s put it all together in a real bank account. The withdraw method throws our exception when funds are too low, and the caller catches it.

class InsufficientFundsException extends Exception {
private final double shortfall;
public InsufficientFundsException(String message, double shortfall) {
super(message);
this.shortfall = shortfall;
}
public double getShortfall() {
return shortfall;
}
}
class BankAccount {
private double balance = 1000;
void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
double shortfall = amount - balance;
throw new InsufficientFundsException( // βœ… throw our own type
"Tried to withdraw " + amount + " but balance is " + balance,
shortfall);
}
balance = balance - amount;
System.out.println("Withdrew " + amount + ". Balance: " + balance);
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
try {
account.withdraw(500); // ok
account.withdraw(2000); // too much, throws
} catch (InsufficientFundsException e) {
System.out.println("Error: " + e.getMessage());
System.out.println("You are short by " + e.getShortfall());
}
}
}

Let’s trace it:

  • withdraw declares throws InsufficientFundsException, because the exception is checked.
  • withdraw(500) is fine. The balance is 1000, so the money comes out and the balance drops to 500.
  • withdraw(2000) fails the check. We work out the shortfall, build the exception with a clear message and that shortfall, and throw it.
  • Control jumps to the catch block. There we print the message and read the field with getShortfall().

Output

Withdrew 500.0. Balance: 500.0
Error: Tried to withdraw 2000.0 but balance is 500.0
You are short by 1500.0

Notice the second withdraw never finished. The moment we threw, the balance = balance - amount line was skipped, so the balance stayed at 500. A failed withdrawal should not change the account.

βš–οΈ Checked or unchecked: which to extend?

You choose the family by what you extend, so make this decision on purpose:

  • Extend Exception (checked) when the caller should be forced to deal with the problem. Business-rule failures fit here: insufficient funds, an invalid order, a missing record. Checked makes the error impossible to forget.
  • Extend RuntimeException (unchecked) when the problem is a programming mistake, or something the caller usually cannot recover from. Bad arguments and broken assumptions fit here. Unchecked keeps the code clean, with no throws everywhere.

So which to reach for? For most business-rule errors a caller ought to handle, checked is a safe default. If forcing throws through many methods feels heavy, unchecked is the alternative. Many modern Java teams lean unchecked for flexibility. Both are valid. Just pick deliberately, because the parent class sets the rule for every caller.

Here is the unchecked version of the same idea. The only change is the parent, and the method no longer needs throws.

class InvalidAgeException extends RuntimeException { // βœ… unchecked
public InvalidAgeException(String message) {
super(message);
}
}
class Registration {
void register(int age) { // βœ… no throws needed for unchecked
if (age < 18) {
throw new InvalidAgeException("Age " + age + " is below the limit of 18");
}
System.out.println("Registered. Age: " + age);
}
}

register throws InvalidAgeException but its header stays clean. That is the trade. You lose the compiler’s reminder, but you gain shorter method signatures.

Naming convention

By convention, exception class names end with Exception, like InsufficientFundsException or InvalidAgeException. This instantly tells any reader the class is an exception. So follow the convention and your code stays easy to read.

⚠️ Common Mistakes

A few custom-exception slip-ups show up again and again. Let’s look at each one.

The first is forgetting to pass the message up to the parent. If you skip super(message), the message is lost and getMessage() returns null.

// ❌ Wrong β€” message never reaches the parent
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
// forgot super(message); getMessage() will return null
}
}
// βœ… Right β€” message is passed up, so getMessage() works
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}

The second is extending the wrong base class. Some people extend Throwable or Error. But Error is for serious system failures the program should not catch, like running out of memory, and Throwable is too low-level for everyday use. So stick to Exception or RuntimeException.

// ❌ Wrong β€” Error is for fatal system problems, not business rules
class InsufficientFundsException extends Error { }
// βœ… Right β€” extend Exception (checked) or RuntimeException (unchecked)
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}

The third is making everything checked. If every tiny problem forces callers to write try-catch or throws, the code fills up with noise. So save checked exceptions for problems the caller can actually recover from. For plain programming mistakes, unchecked is usually the better fit.

The last one is not ending the name with Exception. A class called BadFunds hides the fact that it is an exception. The reader has to dig to find out. InsufficientFundsException says it right away.

βœ… Best Practices

Here are the habits that make custom exceptions pay off.

  • Create one only when a built-in type is too vague. Do not invent an exception for a problem IllegalArgumentException already describes well. A new type should earn its place by being clearer.
  • End the class name with Exception. Follow the standard naming convention so the type explains itself.
  • Always pass a clear message to super. Put the real values in it, like the amount and the balance. A good message turns a debugging session into a quick read.
  • Add fields only when callers need the data. A shortfall or an accountId can help the catch block decide what to do. But do not add fields nobody reads.
  • Choose checked vs unchecked on purpose. Checked for recoverable business errors. Unchecked for programming faults and unrecoverable cases.
  • Reuse the same exception across the layer. One well-named exception used in several methods beats ten near-identical ones.

🧩 What You’ve Learned

Great, that completes the exception handling module. So let’s recap custom exceptions.

  • βœ… A custom exception is a class you create by extending an existing exception class.
  • βœ… Extend Exception for a checked exception, or RuntimeException for an unchecked one.
  • βœ… Always pass the message to the parent with super(message) so getMessage() works.
  • βœ… You can add extra fields (like a shortfall) plus a getter, so the exception carries data, not just text.
  • βœ… You throw and catch custom exceptions exactly like built-in ones, by their specific type.
  • βœ… Name the class ending in Exception and make the error self-documenting.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    How do you create a custom exception in Java?

    Why: A custom exception is a class extending Exception or RuntimeException.

  2. 2

    What does extending Exception (instead of RuntimeException) make your custom exception?

    Why: Extending Exception makes it checked, forcing callers to catch or declare it.

  3. 3

    Why call super(message) in a custom exception's constructor?

    Why: super(message) passes the description to the parent so getMessage() can return it.

  4. 4

    What is the naming convention for a custom exception class?

    Why: By convention, exception class names end with Exception, like InsufficientFundsException.

πŸš€ What’s Next?

You can now handle and create errors. Next we move to one of the most used parts of Java: collections. They are flexible, resizable containers that go far beyond plain arrays. So let’s start with an overview of the Collections Framework.

The Collections Framework in Java

Share & Connect