1 Why We Need Custom Exceptions (The "Grouping" Advantage)
You go to the doctor and say "I am sick." The doctor asks: "Your head? Stomach? Something else?" Why? Because the doctor needs to know the CATEGORY to refer you to the right specialist. A headache goes to the neurologist. A stomach ache goes to the gastroenterologist. The diagnosis matters.
Same with exceptions. If all we get is Exception, Exception, Exception — how do we know which class or object caused it? Was it a bad input? A missing file? An invalid amount? A generic Exception tells us "something went wrong" but not what went wrong or where.
Custom exceptions solve this by giving each error type its own name, its own class, and its own data.
InvalidZakahAmountException or InsufficientFundsException — now you know EXACTLY what happened. You can catch each one separately, log meaningful messages, and take the right corrective action.
2 The Zakah Example
Let's write a Zakah calculator. In Islam, Zakah is 2.5% of wealth, but not all amounts are zakatable — the wealth must be above a threshold (based on the current price of gold, called Nisab). What if someone tries to calculate Zakah on an amount below the threshold?
We could throw ArithmeticException — but that's too general. It tells us "math error" but not "invalid Zakah amount." It groups our error with division-by-zero and overflow errors, which have nothing to do with Zakah rules.
Let's create our own exception: InvalidZakahAmountException.
Exception (or RuntimeException) and add a constructor. That's the entire recipe. The super() call passes the message to the parent Exception class, so getMessage() works automatically.
Now let's use it in a Zakah calculator:
ZakahCalculator.javaNow let's act as a user of this class. We just want to use ZakahCalculator — let's try the simplest thing:
This code does not compile. The compiler gives us:
Error: Unhandled exception type InvalidZakahAmountException
But why? We didn't do anything wrong!
Look at the calculateZakah method signature again:
public static double calculateZakah(double wealth) throws InvalidZakahAmountException
That throws keyword is a warning label. It says: "This method might throw an InvalidZakahAmountException."
And because InvalidZakahAmountException extends Exception (not RuntimeException), it is a checked exception. The compiler says: "I see you are calling a method that may throw a checked exception, but you are not handling it. I will not let you compile until you deal with this."
When you worked with file I/O in Week 3, the compiler forced you to add try-catch around file operations. At that time, you did it without fully understanding why — you just followed the instructions to make the compiler happy.
Today, you finally understand the reason: those file methods declare throws IOException, and IOException is a checked exception. The compiler was protecting you all along!
So how do we fix the compiler error? We have two options:
Catch the exception right here and deal with it.
Pass the responsibility to your caller by adding throws InvalidZakahAmountException to your method signature. This is what we learned in exception propagation.
Let's use Option 1 — handle it with try-catch:
Main.java — with try-catch (compiles!)The compiler is satisfied because we wrapped the call in a try-catch. And we get precise error information — not a vague "math error," but the exact amount and reason. Thanks to the getAmount() method, we can programmatically access the invalid amount and do something useful with it (like showing the user how much more they need).
There are two types of exceptions in Java:
Checked exceptions (extend Exception) — the compiler forces you to handle them. You cannot ignore them. Examples: IOException, SQLException, InvalidZakahAmountException.
Unchecked exceptions (extend RuntimeException) — the compiler does not force you. You can handle them if you want, but the compiler won't complain if you don't. Examples: ArithmeticException, NullPointerException, ArrayIndexOutOfBoundsException.
Let's prove this right now with the same Zakah example.
Experiment What if we change it to RuntimeException?
Let's change one word in our exception class — replace Exception with RuntimeException:
Now let's try the same Main.java without try-catch that gave us a compiler error before:
Main.java — NO try-catchThe compiler does not complain. Because InvalidZakahAmountException now extends RuntimeException, it is unchecked. The compiler does not force you to handle it.
If we pass a valid amount (50,000), everything works fine:
But what if we pass an invalid amount (5,000)?
Main.java — invalid amount, still no try-catchThe compiler let it pass. But when the program runs and hits the invalid amount, it crashes with an unhandled exception:
extends Exception (checked) → compiler error if you don't handle it. You cannot forget.
extends RuntimeException (unchecked) → compiles fine without handling. But if the exception is thrown, your program crashes. The compiler trusted you, and you let it down.
The only difference is one word in the class declaration. That one word changes everything.
3 The Key Rule
Any method that calls another method which may throw a CHECKED exception MUST either:
(1) Handle it with try-catch, OR
(2) Declare it with throws in its own signature
There is no third option. The compiler enforces this — you have no choice.
This is the fundamental rule of checked exceptions. Every time you call a method that declares throws SomeCheckedException, you must decide: do I handle it here, or do I pass it along?
throw — used inside a method body to create and throw an exception object: throw new X("message")
throws — used in the method signature to declare that the method might throw an exception: void myMethod() throws X
One letter difference, completely different meaning!
methodA() calls methodB() which declares throws CheckedException.
Now methodA() MUST either catch it or declare throws CheckedException itself.
If methodA() declares throws, then whoever calls methodA() must also catch or declare. This chain continues until someone handles it.
Think of it like a hot potato — you either catch it or throw it to the next person. Someone eventually must catch it.
Student Q When I create a custom exception, which one should I extend?
A student asks: "When I create my own exception, should I extend Exception (checked) or RuntimeException (unchecked)? How do I decide?"
The answer comes down to one question:
Yes → extends Exception (checked) | No → extends RuntimeException (unchecked)
The error is caused by something outside the program's control: bad user input, a missing file, a network failure, insufficient funds. The caller can do something about it — ask the user for new input, try a different file, retry the connection, suggest a smaller withdrawal.
You want the compiler to force every caller to think about it, because ignoring it would be dangerous.
Examples: IOException, SQLException, InsufficientFundsException, InvalidZakahAmountException
The error is caused by a mistake in the code itself: accessing a null reference, using an invalid array index, passing a negative number where only positive makes sense. The caller shouldn't have let this happen in the first place.
No amount of catching fixes bad code — the code itself needs to be fixed. Forcing try-catch everywhere would just hide the bug.
Examples: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException
We made InvalidZakahAmountException extend Exception (checked). Why? Because the user enters the amount — it's user input, which is external and unpredictable. The program can recover by asking the user to enter a valid amount. The caller should be forced to handle this case.
If instead the problem was a programmer passing null to a method that expects an object — that's a bug. No try-catch fixes that. The programmer needs to fix their code. That would be unchecked.
4 Second Example — InsufficientFundsException
Let's build a more practical example: a bank account that throws a custom exception when you try to withdraw more money than you have.
InsufficientFundsException.javaIf we had used throw new Exception("Not enough money"), the caller would only get a string message. With InsufficientFundsException, the caller can call getShortfall() to find out the exact dollar amount — and perhaps suggest the user deposit more, or offer a smaller withdrawal.
5 Should We Always Use Exceptions?
After learning all this, you might think: "Exceptions are powerful! I should use them everywhere!"
Not so fast. Remember what happens when you write throw new InvalidZakahAmountException(amount):
1. Java creates a new object in memory (the exception object)
2. Java captures the entire call stack (which method called which method called which method...)
3. Java walks back up the call chain looking for a matching catch block
All of this takes time and memory. For truly exceptional situations (a file that doesn't exist, a network that goes down), this cost is worth it. But for situations you can predict and check easily? A simple if statement is much cheaper.
Let's see a concrete example:
Suppose we receive a String and want to print its length. But the string might be null. Should we use an exception or an if statement?
Approach 1 Using an if statement (efficient)
StringDemo.java — using ifThe if statement checks whether name is null before calling length(). No object is created. No stack trace is captured. No exception propagation. Just a simple comparison — almost zero cost.
Approach 2 Using exception handling (expensive)
StringDemo.java — using try-catchBoth approaches print "Name is null." — same output. But the second approach lets name.length() execute, then Java creates a NullPointerException object in memory, captures the entire stack trace, and searches for a matching catch block. All of that work for something we could have prevented with one if check.
If you can predict the problem with a simple condition (if b == 0, if index < 0, if name == null), use an if statement. It's faster, simpler, and clearer.
Use exceptions for situations that are truly exceptional — things you cannot easily predict or prevent:
- A file that was there a moment ago but got deleted
- A network connection that suddenly drops
- A database that runs out of space
- User input that comes from outside your program
You don't pull the fire alarm to tell someone "the door is locked." You check the door handle first. The fire alarm is for actual fires — situations you didn't expect and couldn't prevent with a simple check.
Exceptions are your fire alarm. The if statement is checking the door handle. Use each one where it belongs.
6 Summary
- Custom exceptions let you create domain-specific error types that describe exactly what went wrong.
- Extend
Exceptionfor checked exceptions,RuntimeExceptionfor unchecked. - Checked = the compiler forces every caller to handle it. Unchecked = handling is optional (but the program crashes if unhandled).
- Custom exceptions are just regular classes with constructors — nothing fancy, just inheritance.
- Use them to provide meaningful error information (extra fields, getter methods) beyond a simple message string.
In the next lesson: Practice exercises to test your understanding of everything we've covered in Module 10!