Throwing Exceptions in Java
-
Last Updated: October 14, 2024
-
By: javahandson
-
Series
Learn Java in a easy way
Throwing exceptions in Java lets your own code raise a problem instead of only reacting to one. Learn the throw statement, the throws keyword, checked and unchecked rules, and throw vs throws with clear examples.
So far in this series we have played defence. Something failed, and we caught it. Throwing exceptions in Java flips that around, because now your own code raises the alarm.
Think about a bank teller. You ask to withdraw more money than your account holds. The teller does not quietly hand you nothing and smile. They stop and tell you plainly that the balance is too low.
Your methods need the same voice. When somebody passes a negative age or an empty username, staying silent makes things worse. A clear exception right at that moment saves hours of debugging later.
Java gives you two keywords for this, and beginners mix them up constantly. The throw statement raises an exception. The throws keyword warns callers that a method might raise one. One letter apart, completely different jobs.
This is part 3 of our five-part series. Part 1 covered what exceptions are, and part 2 covered try, catch and finally. Now we learn to raise them ourselves.
We start with a single throw statement and build up to a full call chain. Here is the plan:
You need the basics of try-catch for this article, since we handle what we throw. Everything else builds from scratch.
Throwing an exception means your code creates an exception object and hands it to the JVM. You are saying, out loud, that normal execution cannot continue.
Until now the JVM did that for you. Divide by zero, and it built an ArithmeticException on your behalf. A throw statement lets you do the same thing deliberately.
The object you throw can come from Java’s built-in library, such as IllegalArgumentException. It can also come from a class you write yourself, which is where part 4 of this series goes.
Here is the honest answer. Your method knows the rules of its own job, and nobody else does.
A withdraw method knows that a negative amount makes no sense. A setAge method knows that 300 is not a real age. Only that method can spot the problem, so only it can raise the alarm.
Notice the alternative in every case. Returning a quiet false or -1 pushes the problem downstream, where somebody forgets to check it.
The moment a throw statement runs, your method stops. Java does not finish the remaining lines, and it does not return a value.
Instead the exception starts climbing the call stack, exactly as part 1 described. Java checks each caller in turn for a matching catch block.
Find one, and the program recovers there. Find none, and the JVM prints a stack trace and ends the thread.
The syntax could hardly be simpler. Write the keyword, then an exception object.
throw new ExceptionType("Something useful about what went wrong");Two parts matter here. The type tells the caller what kind of problem occurred, and the message adds the detail.
Throwable.new is almost always there, because you throw an instance and not a class.One curious detail for interviews. Writing throw null; compiles happily, then throws a NullPointerException at runtime. Java has to throw something, so it throws that.
Let us validate an age. Anything below zero should stop the program right there.
package com.javahandson;
public class ThrowExample {
public static void main(String[] args) {
setAge(-5);
}
static void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
System.out.println("Age set to " + age);
}
}
// Output:
// Exception in thread "main" java.lang.IllegalArgumentException: Age cannot be negative: -5
// at com.javahandson.ThrowExample.setAge(ThrowExample.java:10)
// at com.javahandson.ThrowExample.main(ThrowExample.java:5)Read that trace from the top. It names the type, repeats our message, and points at the exact line that threw. Nobody has to guess what went wrong.
Pass 25 instead of -5 and the if never fires. The method prints its line and returns normally.
That message is the first thing a developer reads at midnight. Make it earn its place.
A good message names the value that broke the rule. A bad one just says something failed and leaves you hunting.
// Poor: tells you nothing you did not already know
throw new IllegalArgumentException("Invalid input");
// Better: names the field, the bad value, and the rule
throw new IllegalArgumentException("Age cannot be negative: " + age);Include the offending value whenever you safely can. Skip passwords and card numbers, obviously, but a bad age or filename belongs in the text.
A throw statement behaves a little like return. Control leaves the method immediately, and nothing below it runs.
static void demo() {
System.out.println("before");
throw new IllegalStateException("stopping here");
// System.out.println("after"); // error: unreachable statement
}Uncomment that last line and the code refuses to compile. Java can prove it never runs, so it treats the line as a mistake.
This is genuinely useful. The compiler stops you from writing code that quietly does nothing.
Throw a checked exception and the catch-or-declare rule from part 1 kicks in immediately. The compiler will not let the code through until you deal with it.
Remember the split. A checked exception extends Exception but not RuntimeException, so IOException counts and IllegalArgumentException does not.
Your two options stay the same as before. Handle it here with try-catch, or declare it with throws and let the caller worry.
This program checks for a file and throws an IOException when it finds nothing. The surrounding try-catch deals with it on the spot.
package com.javahandson;
import java.io.File;
import java.io.IOException;
public class ThrowCheckedExample {
public static void main(String[] args) {
try {
File file = new File("file.txt");
if (!file.exists()) {
throw new IOException("file.txt does not exist");
}
System.out.println("File found");
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
// Output: file.txt does not existNotice how the throw and the catch sit in the same method. That works, though it rarely reads well in real code.
Why? Because you already knew about the problem when you threw it. Usually the caller is the one who needs telling.
Now remove the catch block and keep only a finally. The program stops compiling.
public static void main(String[] args) {
try {
File file = new File("file.txt");
if (!file.exists()) {
throw new IOException("file.txt does not exist");
}
} finally {
System.out.println("Clean up the resources");
}
}
// error: unreported exception IOException; must be caught or declared to be thrownThat message comes from the compiler, not from a crash. Nothing ever ran.
A finally block cleans up, but it does not handle anything. Only a catch block or a throws declaration satisfies the compiler.
Throw something that extends RuntimeException and the compiler says nothing at all. No try-catch required, no throws clause required.
That freedom explains why validation code almost always throws unchecked types. Nobody wants a try-catch around every setter call.
Here we guard a division before it can fail. The throw gives a far better message than the JVM would.
package com.javahandson;
public class ThrowUncheckedExample {
public static void main(String[] args) {
System.out.println(divide(10, 0));
}
static int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Cannot divide " + a + " by zero");
}
return a / b;
}
}
// Output:
// Exception in thread "main" java.lang.ArithmeticException: Cannot divide 10 by zero
// at com.javahandson.ThrowUncheckedExample.divide(ThrowUncheckedExample.java:10)
// at com.javahandson.ThrowUncheckedExample.main(ThrowUncheckedExample.java:5)Compare the two messages. Java’s own version says “/ by zero” and stops there. Ours names both numbers, which is far more helpful in a log.
Wrap the call in a try-catch and the program survives instead. The choice belongs to the caller, exactly as it should.
So which family do you pick when you write the throw? A simple question decides it.
Can the caller realistically do something about this? If yes, a checked exception forces them to think. If the problem is really a programming bug, unchecked fits better.
Modern Java leans heavily towards unchecked exceptions. Many popular frameworks, Spring among them, wrap checked exceptions into unchecked ones for exactly this reason.
The throws keyword goes in the method signature, after the parameter list. It announces that this method might raise a given exception.
public void validate() throws IOException {
// body that might throw IOException
}Think of it as a label on a jar. The label does not make the contents safe. It simply warns whoever picks it up.
Once you write that clause, every caller inherits the duty. They must catch IOException or declare it themselves.
A method can declare several exception types. Separate them with commas, not with the pipe used by multi-catch.
static void loadUser(String path) throws IOException, ClassNotFoundException {
// this method might raise either one
}Callers then handle both, either in two catch blocks or in one multi-catch. Part 2 covered both shapes.
Resist the urge to declare a broad type such as throws Exception. It technically compiles, and it tells your callers nothing at all.
Here is the misunderstanding that trips up almost every beginner. A throws clause does not handle, fix, or catch anything.
It only moves the responsibility one level up. Somebody, eventually, still has to write a catch block.
Skip that step and the exception simply reaches the JVM, which prints a stack trace and stops. Declaring an exception is not the same as dealing with it.
Let us build a small chain. The callee method throws, declares the type, and its caller handles it.
package com.javahandson;
import java.io.File;
import java.io.IOException;
public class ThrowsKeywordExample {
public static void main(String[] args) {
caller();
}
static void caller() {
try {
callee();
} catch (IOException e) {
System.out.println("Handled: " + e.getMessage());
}
}
static void callee() throws IOException {
File file = new File("file.txt");
if (!file.exists()) {
throw new IOException("file.txt does not exist");
}
}
}
// Output: Handled: file.txt does not existTwo methods, two different jobs. The callee spots the problem and reports it. The caller decides what to do about it.
That separation is the real point of throws. A low-level method rarely knows whether to retry, log, or show a message.
Now let the middle method duck the problem too. It declares the type instead of catching it.
public static void main(String[] args) {
try {
caller();
} catch (IOException e) {
System.out.println("main handled: " + e.getMessage());
}
}
static void caller() throws IOException { // declares, does not handle
callee();
}
static void callee() throws IOException {
throw new IOException("file.txt does not exist");
}
// Output: main handled: file.txt does not existFollow the chain. The callee throws, the caller passes it along, and main finally catches it.
Every method between the throw and the catch needs that throws clause. Miss one and the compiler stops you.
What if main neither catches nor declares? The compiler refuses, with the message you saw earlier.
public static void main(String[] args) { // no throws, no try-catch
caller();
}
// error: unreported exception IOException; must be caught or declared to be thrownAdd throws IOException to main and it compiles. Now the JVM default handler takes over at runtime.
public static void main(String[] args) throws IOException {
caller();
}
// Output:
// Exception in thread "main" java.io.IOException: file.txt does not exist
// at com.javahandson.ThrowsKeywordExample.callee(ThrowsKeywordExample.java:24)
// at com.javahandson.ThrowsKeywordExample.caller(ThrowsKeywordExample.java:19)
// at com.javahandson.ThrowsKeywordExample.main(ThrowsKeywordExample.java:8)Read that trace bottom to top and you can retrace the whole journey. The exception started in callee, passed through caller, and ended at main.
Declaring it on main is a legal escape hatch, not a solution. Your program still dies, just with a tidier excuse.
You may add a throws clause for an unchecked exception, and Java allows it. Nothing changes about how the code behaves, though.
static void callee() throws ArithmeticException { // legal, but optional
throw new ArithmeticException("Cannot divide a number by zero");
}
static void sameThing() { // identical behaviour
throw new ArithmeticException("Cannot divide a number by zero");
}Both versions compile, and both behave identically. Callers never have to catch either one.
So why write it? Purely as documentation, and honestly Javadoc does that job better with an @throws tag.
This comparison shows up in almost every Java interview. Here it is in one table:
| Aspect | throw | throws |
|---|---|---|
| What it does | Raises an exception right now | Declares that a method might raise one |
| Where it goes | Inside the method body | In the method signature |
| What follows it | An exception object | One or more exception class names |
| How many at once | Exactly one | Several, separated by commas |
| Effect on execution | Stops the method immediately | Changes nothing at runtime |
| Example | throw new IOException("gone"); |
void read() throws IOException |
One line sums it up. The throw statement does the work, while throws writes the warning label.
Most real throw statements sit at the very top of a method. We call these guard clauses, and they reject bad input before any work starts.
static void transfer(String toAccount, double amount) {
if (toAccount == null || toAccount.isBlank()) {
throw new IllegalArgumentException("Target account is required");
}
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive: " + amount);
}
// by this line, both inputs are trustworthy
System.out.println("Transferring " + amount + " to " + toAccount);
}See what those two checks buy you. Everything after them can assume good input, so the real logic stays clean and flat.
Java also gives you a shortcut for null checks. Objects.requireNonNull(name, "name is required") throws NullPointerException with your message in one line.
Java ships with types that cover most validation, so reach for those before writing your own.
IllegalArgumentException when a parameter value breaks the rules.NullPointerException when a required argument arrives as null.IllegalStateException when the object is in the wrong state for this call.UnsupportedOperationException when the method genuinely does not apply.The difference between the first and third catches people out. Bad input from the caller means IllegalArgumentException. A bad situation inside your object means IllegalStateException.
When none of these fit your domain, that is your cue to write a custom exception, which part 4 covers in full.
Why throw at the top rather than letting the code stumble along? Because the failure point and the cause stay close together.
Accept a null name and store it, and the NullPointerException surfaces three classes away. Now you are debugging a symptom instead of the cause.
Throwing early keeps the stack trace pointing at the real culprit. That habit alone will save you more time than any debugger.
Writing throw new Exception("failed") looks harmless. It forces every caller up the chain to declare or catch a type that tells them nothing.
// Poor: the caller learns nothing about what broke
throw new Exception("failed");
// Better: the type itself carries meaning
throw new IllegalArgumentException("Amount must be positive: " + amount);Pick the most specific type that fits. Your callers can then catch precisely what they know how to fix.
Beginners often add a return or a print after a throw, out of habit. The compiler rejects it.
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
// return; // error: unreachable statement
}Nothing follows a throw inside the same block. Delete the extra line and move on.
“Error occurred” is not a message. It repeats what the stack trace already made obvious.
Name the rule and the value that broke it. Six extra words now can save an hour of digging later.
Some codebases sprinkle throws Exception across every signature. It silences the compiler, and it destroys all the information.
Callers can no longer tell which failures are possible, so they write one broad catch and hope. Declare only what a method can genuinely raise.
A throw inside finally replaces whatever exception was already travelling. Your real failure disappears without a trace.
Part 2 covered this trap in detail. Keep finally blocks boring, and never throw from one.
Let us build a tiny bank account that defends itself. Deposits and withdrawals both have rules, and breaking one should stop the operation.
Three rules apply here. A deposit must be positive, a withdrawal must be positive, and you cannot take out more than the balance.
Notice the two different problems. Bad amounts come from the caller, so they earn IllegalArgumentException. An empty balance is a state problem, so it earns IllegalStateException.
package com.javahandson;
public class BankAccount {
private double balance;
void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit must be positive: " + amount);
}
balance += amount;
System.out.println("Deposited " + amount + ", balance is " + balance);
}
void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal must be positive: " + amount);
}
if (amount > balance) {
throw new IllegalStateException("Balance " + balance + " is below " + amount);
}
balance -= amount;
System.out.println("Withdrew " + amount + ", balance is " + balance);
}
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.deposit(100);
try {
account.withdraw(500);
} catch (IllegalArgumentException | IllegalStateException e) {
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
}
account.withdraw(40);
account.deposit(-10); // nothing catches this one
}
}
// Output:
// Deposited 100.0, balance is 100.0
// IllegalStateException: Balance 100.0 is below 500.0
// Withdrew 40.0, balance is 60.0
// Exception in thread "main" java.lang.IllegalArgumentException: Deposit must be positive: -10.0
// at com.javahandson.BankAccount.deposit(BankAccount.java:9)
// at com.javahandson.BankAccount.main(BankAccount.java:38)Walk the four operations in order. The first deposit passes both guards, so the balance climbs to 100.
Withdrawing 500 trips the second guard inside withdraw. That throw ends the method instantly, so the balance never changes, and our multi-catch prints the type and message.
Look carefully at that detail. Because the throw came before balance -= amount, the account cannot end up in a broken state. Guard clauses protect your data as well as your callers.
The third call withdraws 40 successfully and drops the balance to 60. Then the final deposit of -10 throws with nothing to catch it, so the JVM prints a stack trace and stops the program.
A: The throw statement raises an exception right now, and it sits inside the method body followed by an exception object. The throws keyword sits in the method signature and declares which exceptions the method might raise, so callers know to handle them. One does the work, and the other writes the warning label.
A: No. A throw statement takes exactly one exception object, so only one travels at a time. The throws keyword is different, because it accepts several class names separated by commas.
A: Any object whose class descends from java.lang.Throwable, which means every Exception and every Error. A class that does not extend Throwable cannot appear in a throw statement. Writing throw null compiles, but it raises a NullPointerException at runtime.
A: No, and this is the most common misunderstanding. A throws clause only passes the responsibility to the caller. Somebody up the chain still needs a catch block, otherwise the exception reaches the JVM, which prints a stack trace and ends the thread.
A: No. The compiler ignores unchecked exceptions, so a method throwing ArithmeticException needs no declaration. You may still add the clause as documentation, though a Javadoc @throws tag communicates it better.
A: The code compiles, and at runtime the exception reaches the JVM default handler. That handler prints the stack trace and terminates the thread, so the program still fails. Declaring it on main is an escape hatch, not a fix.
A: A throw ends the method immediately, so any statement directly after it in the same block can never run. The compiler proves this and reports “unreachable statement”. Remove the extra line to fix it.
A: Throw a checked exception when the caller can realistically recover, such as retrying a missing file. Throw an unchecked exception when the problem is a programming mistake, such as a negative amount or a null argument. Modern Java and frameworks like Spring lean towards unchecked types.
A: IllegalArgumentException means the caller passed a bad value, such as a negative amount. IllegalStateException means the object itself is in the wrong condition for the call, such as withdrawing from an account with too little balance. The first blames the argument, and the second blames the situation.
A: The type itself carries meaning, and Exception carries none. Every caller must then declare or catch a broad type that hides what actually went wrong, so they cannot handle one failure differently from another. Always throw the most specific type that fits.
Let us wrap up what we covered. The throw statement raises an exception yourself, and it ends the current method on the spot.
You may throw anything descending from Throwable. Throw a checked type and the catch-or-declare rule applies, while an unchecked type leaves the compiler silent.
The throws keyword sits in the method signature and warns callers about the types a method might raise. It declares, but it never handles. Somebody still has to catch.
Most real throws are guard clauses at the top of a method. Reach for IllegalArgumentException on bad input, IllegalStateException on a bad situation, and always name the offending value in the message.
Part 4 of this series takes the next step. When no built-in type fits your problem, you write your own exception class and chain it to the cause underneath.