Skip to content
Ameen Abozaid
PublishedBEG-029-L04

Throwing and declaring

  • Lesson 4 of 8
  • 12 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

In lesson 3 you handled exceptions thrown by someone else, such as Integer.parseInt. But the code that raises the alarm has to be written by somebody, and often that somebody is you. When a method receives a value it cannot accept, or when a business rule is broken, the method should throw an exception. When that exception is a checked one, the method must also declare it, so that callers know to prepare a plan.

In our fire-alarm picture: catch is the person who responds to the alarm. throw is pulling the alarm. throws is the sign on the door saying "alarm may sound in this room, so know your plan".

Why this matters: a method that quietly returns a wrong result or prints a message and carries on is far harder to debug than one that stops with a clear explanation. Throwing early, with a message that names the bad value, is one of the most useful habits in backend work.

This lesson builds on the hierarchy (lesson 2), try/catch (lesson 3), and the validation with IllegalArgumentException and IllegalStateException from the encapsulation module.

Learning Objectives

By the end of this lesson you will be able to:

  • Throw an exception with throw new ... and explain what happens to the rest of the method.
  • Write a message that says what was wrong and which value caused it.
  • Choose between IllegalArgumentException and IllegalStateException.
  • Declare a checked exception with throws and explain what callers must do.
  • Decide whether a caller should catch an exception or declare it too.
  • Recognize misuse of exceptions as normal control flow.

Written Explanation

The throw statement

To raise an exception, create an object and throw it:

java
throw new IllegalArgumentException("age must be between 0 and 150, was " + age);

throw takes one thing: an object of a type that descends from Throwable. The Oracle tutorial states that only such objects can be thrown, and that exceptions are always thrown with this keyword, whether they come from your code, a library, or the runtime. As soon as throw executes, the method stops. Nothing after it in that method runs, and Java starts searching for a catch, as in lesson 1.

A common pattern is the guard clause: check the bad case at the top, throw, and let the rest of the method assume everything is fine.

java
void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("age must be between 0 and 150, was " + age);
    }
    this.age = age;
}

This is the same idea as the invariants in the encapsulation module. Failing at the point where the bad value arrives, rather than later, makes the cause easy to find.

Messages that help

The message is the first thing a person sees. A useful message says what was wrong and which value caused it, and ideally what was expected.

PoorBetter
"Error""age must be between 0 and 150, was -4"
"Invalid input""Quantity must be positive, was 0"
"" (empty)"Cannot withdraw 500.0 from a balance of 70.0"

A good message costs one string concatenation.

Which exception to throw

Use an existing exception when its name describes your problem:

  • IllegalArgumentException: the argument is wrong, whatever the object's state. Example: a negative deposit.
  • IllegalStateException: the argument is fine, but the state of the object does not allow the call now. Example: withdrawing more than the balance (until lesson 5 gives us a better type).
  • Lesson 2's checked exceptions, such as IOException, for problems with the outside world.

One note: the Oracle tutorial says that most applications should not throw runtime exceptions or subclass RuntimeException, and treats them as for incorrect use of an API. Standard Java libraries themselves throw IllegalArgumentException for bad arguments, and the earlier encapsulation module used it the same way. Treat the advice as part of the long-running debate over checked versus unchecked: follow what your team and framework do, and be consistent.

The throws clause

If a method can throw a checked exception and does not catch it, the method must declare it. The throws keyword goes after the parameter list:

java
static String load(String name) throws IOException {
    if (!name.endsWith(".cfg")) {
        throw new IOException("Unsupported file type (expected .cfg): " + name);
    }
    return "settings from " + name;
}

The declaration is part of the method's contract: it tells every caller "this can fail with an IOException". The Oracle tutorial notes that declaring unchecked exceptions is optional. Either form compiles. Some teams list important unchecked exceptions in throws or in documentation as a hint to readers, but the compiler never forces it.

What callers must do

When you call a method that declares a checked exception, the Catch or Specify Requirement (lesson 2) applies to your code. You have exactly two choices:

  1. Catch it with try/catch, and handle it here.
  2. Declare it in your own throws clause, passing the decision up to your caller.

If you do neither, the compiler stops you:

text
NoPlan.java:9: error: unreported exception IOException; must be caught or declared to be thrown
        System.out.println(load("a.cfg"));
                               ^

Which choice is right? Handle it where you can do something sensible about it. A method deep in the code usually cannot decide what the user should see, so it declares the exception. A method near the top (often main) can show a message or retry, so it catches. Declaring everything with throws Exception is a poor habit: it announces "anything may go wrong" and stops the compiler helping you.

Exceptions are not for normal flow

An exception is for something unexpected that stops the normal path. Do not use one to end a loop or to return a normal answer:

java
// Poor: using an exception as the loop exit
try {
    for (int i = 0; ; i++) {
        System.out.println(values[i]);
    }
} catch (ArrayIndexOutOfBoundsException e) {
    // reached the end
}

// Better
for (int i = 0; i < values.length; i++) {
    System.out.println(values[i]);
}

The first version hides the real end condition and would swallow a bug if an index were wrong for another reason. Rule: if a plain if or loop condition can express it, use that.

Examples

Example 1: a guard clause with a good message (simple)

java
public class Age {
    private int age;

    void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("age must be between 0 and 150, was " + age);
        }
        this.age = age;
    }

    public static void main(String[] args) {
        Age person = new Age();
        person.setAge(30);
        System.out.println("age set to " + person.age);
        try {
            person.setAge(-4);
        } catch (IllegalArgumentException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
        person.setAge(900);
        System.out.println("not reached");
    }
}

Real output:

text
age set to 30
Rejected: age must be between 0 and 150, was -4
Exception in thread "main" java.lang.IllegalArgumentException: age must be between 0 and 150, was 900
	at Age.setAge(Age.java:6)
	at Age.main(Age.java:20)

The first call succeeds. The second throws, and the catch shows the message. The third is not caught, so the program ends and "not reached" is never printed. Notice that the trace line Age.java:6 points at the throw statement, which is where the exception was created.

Example 2: declaring a checked exception (practical)

java
import java.io.IOException;

public class Load {
    static String load(String name) throws IOException {
        if (!name.endsWith(".cfg")) {
            throw new IOException("Unsupported file type (expected .cfg): " + name);
        }
        return "settings from " + name;
    }

    static void start(String name) throws IOException {
        System.out.println("Starting with " + load(name));
    }

    public static void main(String[] args) {
        try {
            start("app.cfg");
            start("app.txt");
        } catch (IOException e) {
            System.out.println("Cannot start: " + e.getMessage());
        }
    }
}

Real output:

text
Starting with settings from app.cfg
Cannot start: Unsupported file type (expected .cfg): app.txt

The exception travels through two levels. load throws and declares it. start calls load and has no good response, so it declares the same exception rather than catching it. main is the place that talks to the user, so it catches and prints a message. The compiler checked every step: remove throws IOException from start, and it would refuse to compile, exactly like the error shown above. (The check in load is a stand-in for real file access, which arrives in lesson 6.)

Example 3: two different failures in the bank (realistic)

java
public class Bank {
    static class Account {
        private double balance;

        void deposit(double amount) {
            if (amount <= 0) {
                throw new IllegalArgumentException("Deposit must be positive, was " + amount);
            }
            balance += amount;
        }

        void withdraw(double amount) {
            if (amount <= 0) {
                throw new IllegalArgumentException("Withdrawal must be positive, was " + amount);
            }
            if (amount > balance) {
                throw new IllegalStateException(
                    "Cannot withdraw " + amount + " from a balance of " + balance);
            }
            balance -= amount;
        }
    }

    public static void main(String[] args) {
        Account a = new Account();
        a.deposit(100);
        double[] requests = {30, -5, 500, 70};
        for (double r : requests) {
            try {
                a.withdraw(r);
                System.out.println("Withdrew " + r + ", balance " + a.balance);
            } catch (IllegalArgumentException e) {
                System.out.println("Bad request: " + e.getMessage());
            } catch (IllegalStateException e) {
                System.out.println("Refused: " + e.getMessage());
            }
        }
    }
}

Real output:

text
Withdrew 30.0, balance 70.0
Bad request: Withdrawal must be positive, was -5.0
Refused: Cannot withdraw 500.0 from a balance of 70.0
Withdrew 70.0, balance 0.0

withdraw throws two different exceptions for two different reasons, and the loop handles each with its own message and carries on. A failed withdrawal leaves the balance untouched: both checks happen before balance -= amount, so an exception never leaves half-done work behind. Neither exception is checked, so withdraw has no throws clause.

Backend Insight

Backend code is layered: a controller calls a service, which calls a repository, which talks to a database. Each layer either handles a failure or lets it travel up. The bottom layer throws, the middle layers mostly pass the exception along, and the top layer turns it into a response and a log entry.

Validation at the boundary is another everyday use: a service that throws IllegalArgumentException at its entry point guarantees nothing below it works on garbage. And because messages end up in logs read by operations staff, "Cannot withdraw 500.0 from a balance of 70.0" saves real time, while "Error" costs it.

Common Mistakes

Mistake 1: Messages that say nothing

  • Incorrect: throw new IllegalArgumentException();, or the message "bad".
  • Correct: say what was wrong and include the value.

Mistake 2: throws Exception everywhere

It compiles and silences the compiler, but callers learn nothing about what can fail, and they are forced to handle everything. Declare the specific checked exception.

Mistake 3: Throwing after half the work is done

If a method changes some state and then throws, the object is left inconsistent. Do all the checks before changing anything.

Mistake 4: Exceptions as loop exits or return values

Using an exception to signal "end of data" or "not found" in an ordinary search tangles the code. Use a condition or a normal return value.

Mistake 5: Putting secrets in the message

Passwords, tokens, and full card numbers must not appear in a message, because messages are printed and logged.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. What does the throws keyword in a method header tell the reader?

Show solution A1

A1. That the method can throw that (checked) exception, so callers must catch it or declare it too.

A2. What happens to the lines after throw in the same method?

Show solution A2

A2. They do not run. The method stops at the throw and the exception propagates.

A3. Which is more helpful, "Invalid price" or "Price must be above 0, was -9.99"? Say why in one sentence.

Show solution A3

A3. The second, because it says which value was wrong and what was expected, so nobody has to debug to find out.

Level B: Application

B1. Write a method static void setQuantity(int quantity) for an order that rejects zero or negative values with a clear message. Which exception type fits?

Show solution B1

B1.

java
static void setQuantity(int quantity) {
    if (quantity <= 0) {
        throw new IllegalArgumentException("Quantity must be above 0, was " + quantity);
    }
    // store it
}

IllegalArgumentException fits, because the argument itself is wrong.

B2. The method static void save(String path) throws IOException is called from static void backup(). Show two ways to write backup so that it compiles.

Show solution B2

B2. Catch: static void backup() { try { save("data.txt"); } catch (IOException e) { System.out.println("Backup failed: " + e.getMessage()); } }. Declare: static void backup() throws IOException { save("data.txt"); }, which hands the decision to the caller.

B3. For each, choose IllegalArgumentException or IllegalStateException: (a) setDiscount(150) where a discount must be 0 to 100; (b) ship() called on an order that has already been shipped.

Show solution B3

B3. (a) IllegalArgumentException: the argument 150 is outside the allowed range. (b) IllegalStateException: the call itself is fine, but the order's state (already shipped) forbids it.

Level C: Challenge

C1. This method is called in a loop. Rewrite it so it no longer uses an exception for normal flow, and explain the change.

java
static boolean contains(int[] data, int target) {
    try {
        for (int i = 0; ; i++) {
            if (data[i] == target) return true;
        }
    } catch (ArrayIndexOutOfBoundsException e) {
        return false;
    }
}
Show solution C1

C1.

java
static boolean contains(int[] data, int target) {
    for (int i = 0; i < data.length; i++) {
        if (data[i] == target) return true;
    }
    return false;
}

The loop condition now states the real end, and a genuine bug (such as a wrong index elsewhere) is no longer swallowed.

C2. Write static double divide(double a, double b) that refuses a zero divisor with a helpful message, then write a main that calls it with 10, 4 and 5, 0, catching the failure and printing the message. Run it and record the output.

Show solution C2

C2.

java
static double divide(double a, double b) {
    if (b == 0) {
        throw new IllegalArgumentException("Cannot divide " + a + " by zero");
    }
    return a / b;
}

public static void main(String[] args) {
    try {
        System.out.println(divide(10, 4));
        System.out.println(divide(5, 0));
    } catch (IllegalArgumentException e) {
        System.out.println("Refused: " + e.getMessage());
    }
}

Expected output: 2.5 then Refused: Cannot divide 5.0 by zero.

Mini Challenge

Take a class from an earlier module (the bank Account is a good choice, or a Product with a price and stock). Add at least three guard clauses to its methods, each throwing a suitable exception with a message that names the bad value. Write a small main that calls each method with one good and one bad value, and catches each failure with the narrowest type. Then add one method that declares a checked exception (you can simulate the failure like load in Example 2) and make main handle it.

Check your work: no message is empty or generic, every guard comes before any state change, and main has no catch block that is empty or catches plain Exception.

Summary & Next Lesson

Key takeaways

  • throw new SomeException("message") stops the method and starts the search for a handler.
  • A good message says what was wrong and which value caused it, and never includes secrets.
  • IllegalArgumentException for a bad argument; IllegalStateException for a bad moment.
  • A method that may throw a checked exception declares it with throws. Callers must catch it or declare it too. Declaring unchecked exceptions is optional.
  • Handle an exception where you can do something sensible about it; otherwise let it travel up.
  • Check before changing state, and do not use exceptions as normal control flow.

Next lesson

Built-in exceptions fit many problems, but business rules often deserve their own name. The next lesson, "Custom exceptions", shows how to write an exception class such as InsufficientFundsException and why to pass the original cause along.

Self-check

  1. What does throw need as its argument?
  2. What are the two things a caller of a method with throws IOException can do?
  3. Name two things a good exception message contains.
  4. Why should checks happen before state changes?
  5. Why is for (int i = 0; ; i++) ended by a caught exception a bad design?
Show answers
  1. An object whose class descends from Throwable.
  2. Catch it with try/catch, or declare it in its own throws clause.
  3. What was wrong and the value involved (and ideally what was expected).
  4. So an exception never leaves the object half-changed and inconsistent.
  5. An ordinary loop condition can express the end; using an exception hides the real condition and can swallow genuine bugs.

References

  • Oracle, The Java Tutorials, How to Throw Exceptions. Fetched. Supports that exceptions are thrown with throw, that only Throwable objects can be thrown, and the tutorial's remark that most applications should not throw runtime exceptions.
  • Oracle, The Java Tutorials, Specifying the Exceptions Thrown by a Method. Fetched. Supports the placement and purpose of throws, that a checked exception not caught must be declared, and that declaring unchecked exceptions is optional.
  • Oracle, The Java Tutorials, The Catch or Specify Requirement. Fetched. Supports the two choices (catch or declare) for checked exceptions.
  • Examples 1 to 3 and the compiler message were compiled and run with JDK 21.0.10. Exercise solutions C1 and C2 were also run. The advice on where to handle exceptions and on messages is common engineering guidance, not a quotation from a specific source.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.