Throwing Exceptions in Java

  • Last Updated: October 14, 2024
  • By: javahandson
  • Series
img

Throwing Exceptions in Java

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.

1. Introduction

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.

1.1 What This Article Covers

We start with a single throw statement and build up to a full call chain. Here is the plan:

  • What throwing an exception actually does to your program
  • The throw statement, its syntax, and the message you attach
  • Throwing a checked exception, and what the compiler demands
  • Throwing an unchecked exception, and why the compiler stays quiet
  • The throws keyword, including declaring several types at once
  • How a declared exception travels from method to method up to main
  • throw vs throws, side by side in a table
  • Guard clauses, common mistakes, and interview questions

You need the basics of try-catch for this article, since we handle what we throw. Everything else builds from scratch.

2. What Throwing an Exception Means

2.1 Raising the Alarm Yourself

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.

2.2 Why Would You Throw One?

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.

  • Bad arguments arrived, such as a null name or a negative price.
  • The object sits in the wrong state for the operation you asked for.
  • A business rule failed, like withdrawing more than the balance.
  • Something outside broke, and this method cannot recover from it.

Notice the alternative in every case. Returning a quiet false or -1 pushes the problem downstream, where somebody forgets to check it.

2.3 What Java Does Next

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.

3. The throw Statement

3.1 The Syntax

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.

  • You may throw any object whose class descends from Throwable.
  • The message is optional, since most exception classes offer a no-argument constructor.
  • 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.

3.2 Throwing Your First Exception

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.

3.3 The Message Matters

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.

3.4 throw Ends the Method on the Spot

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.

4. Throwing a Checked Exception

4.1 The Compiler Steps In

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.

4.2 Handling It Right Here

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 exist

Notice 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.

4.3 What Happens Without a Handler

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 thrown

That 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.

5. Throwing an Unchecked Exception

5.1 No Compiler Nagging

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.

5.2 An Unchecked Throw in Action

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.

5.3 Which Type Should You Throw?

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.

  • Bad arguments passed by a programmer, so throw unchecked.
  • A missing file the caller could retry or replace, so throw checked.
  • An impossible internal state, which means unchecked again.
  • A failed business rule the caller must handle, where checked earns its keep.

Modern Java leans heavily towards unchecked exceptions. Many popular frameworks, Spring among them, wrap checked exceptions into unchecked ones for exactly this reason.

6. The throws Keyword

6.1 Declaring Instead of Handling

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.

6.2 Declaring More Than One Type

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.

6.3 throws Handles Nothing

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.

7. How the Exception Travels Up

7.1 Passing It to the Caller

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 exist

Two 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.

7.2 Passing It All the Way to main

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 exist

Follow 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.

7.3 When Nobody Declares It

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 thrown

Add 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.

7.4 throws on Unchecked Exceptions

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.

8. throw vs throws

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.

9. Validation: The Everyday Use of throw

9.1 Guard Clauses

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.

9.2 Picking the Right Built-in Type

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.

9.3 Fail Fast Beats Fail Later

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.

10. Common Mistakes and Pitfalls

10.1 Throwing Exception or Throwable

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.

10.2 Unreachable Code After throw

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.

10.3 A Message That Says Nothing

“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.

10.4 Declaring throws on Everything

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.

10.5 Throwing From a finally Block

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.

11. A Practical Walkthrough

11.1 The Task

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.

11.2 The Code

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)

11.3 Reading the Output

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.

  • Each guard clause names the rule and the offending value.
  • Two different problems get two different exception types.
  • A throw stops the method before it can corrupt the balance.
  • An uncaught throw still ends the program, exactly as part 1 described.

12. Interview Questions

Q: What is the difference between throw and throws in Java?

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.

Q: Can we throw multiple exceptions with a single throw statement?

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.

Q: What can you throw in Java?

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.

Q: Does the throws keyword handle an exception?

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.

Q: Do we need throws for unchecked exceptions?

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.

Q: What happens if the main method declares throws?

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.

Q: Why does code after a throw statement fail to compile?

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.

Q: When should you throw a checked exception instead of an unchecked one?

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.

Q: What is the difference between IllegalArgumentException and IllegalStateException?

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.

Q: Why is throwing Exception considered bad practice?

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.

13. Conclusion

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.

Further Reading

Leave a Comment