الإلقاء والتصريح
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
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
IllegalArgumentExceptionandIllegalStateException. - Declare a checked exception with
throwsand 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:
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.
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.
| Poor | Better |
|---|---|
"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:
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:
- Catch it with
try/catch, and handle it here. - Declare it in your own
throwsclause, passing the decision up to your caller.
If you do neither, the compiler stops you:
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:
// 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)
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:
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)
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:
Starting with settings from app.cfg
Cannot start: Unsupported file type (expected .cfg): app.txtThe 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)
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:
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.0withdraw 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?
أظهر الحل 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?
أظهر الحل 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.
أظهر الحل 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?
أظهر الحل B1
B1.
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.
أظهر الحل 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.
أظهر الحل 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.
static boolean contains(int[] data, int target) {
try {
for (int i = 0; ; i++) {
if (data[i] == target) return true;
}
} catch (ArrayIndexOutOfBoundsException e) {
return false;
}
}أظهر الحل C1
C1.
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.
أظهر الحل C2
C2.
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.
IllegalArgumentExceptionfor a bad argument;IllegalStateExceptionfor 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
- What does
throwneed as its argument? - What are the two things a caller of a method with
throws IOExceptioncan do? - Name two things a good exception message contains.
- Why should checks happen before state changes?
- Why is
for (int i = 0; ; i++)ended by a caught exception a bad design?
أظهر الإجابات
- An object whose class descends from
Throwable. - Catch it with
try/catch, or declare it in its ownthrowsclause. - What was wrong and the value involved (and ideally what was expected).
- So an exception never leaves the object half-changed and inconsistent.
- 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 onlyThrowableobjects 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.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.