Java throw Keyword
What throw Does
So far, exceptions have been thrown by the JVM or by library methods — parseInt throwing NumberFormatException, an array access throwing ArrayIndexOutOfBoundsException. The throw keyword lets your own code throw an exception.
throw new ExceptionType("message describing what went wrong");
| Part | Meaning |
|---|---|
throw | A statement that throws an exception object immediately |
new ExceptionType(...) | Creates the exception object. It must be a Throwable (in practice an Exception subclass) |
"message..." | The detail message, returned later by getMessage() |
Why you need it: methods have rules. A ticket price can't be computed for age -4; an order can't be shipped before it's paid; a bank transfer can't be negative. When a method receives a request it can't fulfil correctly, it has three options: return a wrong value, return a special code such as -1, or throw an exception. Returning a wrong value spreads corrupt data. Special codes are easy for callers to ignore. Throwing stops the operation at the exact point where the problem was detected, with a message explaining why, and the caller can't accidentally continue as if nothing happened.
A First Example: Validating an Argument
public class Ticket {
static double price(int age) {
if (age < 0 || age > 130) {
throw new IllegalArgumentException("age must be 0-130, got " + age);
}
return age < 12 ? 150.0 : 300.0;
}
public static void main(String[] args) {
System.out.println(price(8));
System.out.println(price(35));
System.out.println(price(-4));
System.out.println("not reached");
}
}
Output:
150.0
300.0
Exception in thread "main" java.lang.IllegalArgumentException: age must be 0-130, got -4
at Ticket.price(Ticket.java:4)
at Ticket.main(Ticket.java:12)
How it works:
- For
8and35, theifcondition is false, so the method calculates and returns a price. - For
-4, the condition is true.new IllegalArgumentException(...)creates an exception object, andthrowthrows it. throwbehaves like any exception: the method stops at that line —returnis never reached — and the exception propagates to the caller. Nothing inmaincatches it, so the program ends with a stack trace, and"not reached"is never printed.- The top stack frame,
Ticket.price(Ticket.java:4), is thethrowline. The JVM records the stack trace when the exception object is created (inThrowable's constructor), which is why you normally create and throw in the same statement.
The message includes the bad value (got -4). A message such as "invalid age" would force whoever reads the log to guess which value was wrong; including the actual input makes the problem obvious.
This kind of check at the start of a method is called a guard clause or precondition check. It keeps invalid data out of the rest of the method, so the remaining code can assume its inputs are valid.
Rules of the throw Statement
Statements after throw are unreachable
public class Unreach {
static void check(int qty) {
if (qty <= 0) {
throw new IllegalArgumentException("qty must be positive");
System.out.println("checked");
}
}
}
Compiler output:
Unreach.java:5: error: unreachable statement
System.out.println("checked");
^
1 error
Like return, throw always transfers control out of the current block, so code directly after it can never run. Java treats unreachable code as a compile-time error.
Only Throwable objects can be thrown
public class NotThrowable {
static void fail() {
throw "something went wrong";
}
}
Compiler output:
NotThrowable.java:3: error: incompatible types: String cannot be converted to Throwable
throw "something went wrong";
^
1 error
Unlike JavaScript or Python, Java can only throw instances of Throwable and its subclasses, because the JVM's exception machinery depends on the stack trace and type information that Throwable provides. If you write throw null;, the JVM throws a NullPointerException instead.
Throwing a checked exception must be declared or caught
public class CheckedThrow {
static void connect(String host) {
if (host.isBlank()) {
throw new java.io.IOException("no host");
}
}
}
Compiler output:
CheckedThrow.java:4: error: unreported exception IOException; must be caught or declared to be thrown
throw new java.io.IOException("no host");
^
1 error
IOException is checked, so the method must either catch it or declare it with throws IOException in its signature. Unchecked exceptions (RuntimeException subclasses) don't have this requirement. The next lesson explains throws.
Choosing Which Exception to Throw
Prefer the standard exceptions from the JDK when their meaning fits. Other developers already know what they mean, and frameworks handle them consistently.
| Exception | Throw it when | Example |
|---|---|---|
IllegalArgumentException | An argument has an invalid value | Negative quantity, malformed email |
NullPointerException | An argument that must not be null is null | Objects.requireNonNull(name, "name") |
IllegalStateException | The arguments are fine, but the object is in the wrong state for this call | Shipping an unpaid order, using a closed connection |
IndexOutOfBoundsException | An index is outside the valid range | Objects.checkIndex(i, size) |
UnsupportedOperationException | The operation isn't supported by this implementation | Modifying a read-only view |
ArithmeticException | An arithmetic rule is broken | Math.addExact overflow |
| A custom exception | A business rule needs its own type the caller can catch specifically | InsufficientFundsException (Custom Exceptions lesson) |
The distinction between IllegalArgumentException and IllegalStateException is the one developers most often blur. Ask: would this call work with different arguments? If yes, the argument is wrong (IllegalArgumentException). If the same arguments would work at another time, the object's state is wrong (IllegalStateException).
Built-in Helpers That Throw for You
The JDK has methods that perform common checks and throw the right exception, which keeps guard clauses short. switch expressions can also throw from a branch.
import java.util.*;
public class Guards {
static String initials(String fullName) {
Objects.requireNonNull(fullName, "fullName must not be null");
StringBuilder sb = new StringBuilder();
for (String part : fullName.trim().split("\\s+")) sb.append(part.charAt(0));
return sb.toString().toUpperCase();
}
static String seat(List<String> seats, int index) {
return seats.get(Objects.checkIndex(index, seats.size()));
}
static int discount(String tier) {
return switch (tier) {
case "GOLD" -> 20;
case "SILVER" -> 10;
case "BASIC" -> 0;
default -> throw new IllegalArgumentException("unknown tier: " + tier);
};
}
public static void main(String[] args) {
List<Runnable> calls = List.of(
() -> System.out.println(initials("asha rao")),
() -> System.out.println(initials(null)),
() -> System.out.println(seat(List.of("A1", "A2"), 1)),
() -> System.out.println(seat(List.of("A1", "A2"), 2)),
() -> System.out.println(discount("GOLD")),
() -> System.out.println(discount("PLATINUM")));
for (Runnable call : calls) {
try {
call.run();
} catch (RuntimeException e) {
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
}
Output:
AR
NullPointerException: fullName must not be null
A2
IndexOutOfBoundsException: Index 2 out of bounds for length 2
20
IllegalArgumentException: unknown tier: PLATINUM
Objects.requireNonNull(value, message)returnsvalueif it isn'tnull, otherwise throwsNullPointerExceptionwith your message. Without it,initials(null)would still fail atfullName.trim(), but checking first produces a clear message and fails before any work is done. It's common in constructors:this.name = Objects.requireNonNull(name, "name");.Objects.checkIndex(index, length)(Java 9+) returns the index if0 <= index < length, otherwise throwsIndexOutOfBoundsExceptionwith a standard message.throwin aswitchexpression. Aswitchexpression must produce a value for every input. Thedefault -> throw ...branch is allowed because athrownever completes normally, so it doesn't need to produce a value. This makes unknown values fail loudly instead of silently falling through to some default discount.
Other places where Java accepts a throw in place of a value: Optional.orElseThrow(() -> new SomeException(...)), and lambda bodies such as () -> { throw new IllegalStateException(); }. A throw can't be used inside the conditional operator (cond ? value : throw ... doesn't compile) — use an if instead.
Practical Example: Enforcing Business Rules in a Class
A class is responsible for keeping its own data valid. Throwing from constructors and methods is how it refuses invalid states. This order class allows only valid transitions: CREATED → PAID → SHIPPED.
import java.util.*;
public class OrderFlow {
enum Status { CREATED, PAID, SHIPPED, CANCELLED }
static class Order {
private final String id;
private final List<String> items = new ArrayList<>();
private Status status = Status.CREATED;
Order(String id) {
if (id == null || !id.matches("ORD-\\d{4}")) {
throw new IllegalArgumentException("invalid order id: " + id);
}
this.id = id;
}
void addItem(String sku) {
requireStatus(Status.CREATED, "add items");
items.add(sku);
}
void pay() {
requireStatus(Status.CREATED, "pay");
if (items.isEmpty()) {
throw new IllegalStateException(id + ": cannot pay for an empty order");
}
status = Status.PAID;
}
void ship() {
requireStatus(Status.PAID, "ship");
status = Status.SHIPPED;
}
private void requireStatus(Status expected, String action) {
if (status != expected) {
throw new IllegalStateException(id + ": cannot " + action + " when status is " + status);
}
}
@Override public String toString() { return id + " " + status + " " + items; }
}
static void attempt(String label, Runnable step) {
try {
step.run();
System.out.println("ok " + label);
} catch (IllegalArgumentException | IllegalStateException e) {
System.out.println("FAIL " + label + " -> " + e.getMessage());
}
}
public static void main(String[] args) {
attempt("create ORD-12", () -> new Order("ORD-12"));
Order o = new Order("ORD-1001");
attempt("pay empty order", o::pay);
attempt("add pen", () -> o.addItem("PEN-01"));
attempt("ship before paying", o::ship);
attempt("pay", o::pay);
attempt("add after paying", () -> o.addItem("INK-03"));
attempt("ship", o::ship);
System.out.println(o);
}
}
Output:
FAIL create ORD-12 -> invalid order id: ORD-12
FAIL pay empty order -> ORD-1001: cannot pay for an empty order
ok add pen
FAIL ship before paying -> ORD-1001: cannot ship when status is CREATED
ok pay
FAIL add after paying -> ORD-1001: cannot add items when status is PAID
ok ship
ORD-1001 SHIPPED [PEN-01]
Step by step:
- Constructor validation.
"ORD-12"doesn't match the patternORD-followed by four digits, so the constructor throwsIllegalArgumentException. When a constructor throws, thenewexpression never produces an object, so an invalidOrdercan never exist anywhere in the program. - State checks.
requireStatuscentralises the rule "this action is only allowed in this status." It throwsIllegalStateException, because the arguments are fine — it's the order's current state that forbids the action. - A rule involving data. Paying is allowed in
CREATED, but not with zero items, sopay()adds a second check. - The state changes only after all checks pass. In
pay(),status = Status.PAIDis the last line. If any check throws, the method exits before modifying anything, so a failed call leaves the object exactly as it was. Validating first and mutating last is what makes these failures safe. - The final state shows only the valid operations took effect: one item, status
SHIPPED.
In a web application, a global exception handler would typically turn IllegalArgumentException into HTTP 400 (Bad Request) and IllegalStateException into HTTP 409 (Conflict), so choosing the right type also determines what the client sees.
Rethrowing an Exception
Sometimes a catch block needs to do something — log, roll back, update a metric — but can't actually resolve the problem. It can rethrow the same exception with throw e; so callers still see the failure:
import java.io.*;
import java.nio.file.*;
public class Rethrow {
static String read(String name) throws IOException {
try {
return Files.readString(Path.of(name));
} catch (IOException e) {
System.out.println("log: failed to read " + name + " (" + e.getClass().getSimpleName() + ")");
throw e;
}
}
public static void main(String[] args) {
try {
read("missing-report.txt");
} catch (IOException e) {
System.out.println("main: " + e);
}
}
}
Output:
log: failed to read missing-report.txt (NoSuchFileException)
main: java.nio.file.NoSuchFileException: missing-report.txt
Rethrowing keeps the original exception object, so its type (NoSuchFileException), message and stack trace are preserved. Because IOException is checked, read must declare throws IOException.
Be careful not to log and rethrow at every level of a call chain — the same failure then appears many times in the logs. Usually one layer logs and the others just propagate.
Precise rethrow (Java 7+)
If a catch clause catches a broad type like Exception and rethrows the same variable unchanged, the compiler works out which checked exceptions can actually reach that point:
import java.io.IOException;
import java.nio.file.*;
public class Precise {
static String load(String name) throws IOException {
try {
return Files.readString(Path.of(name));
} catch (Exception e) {
System.out.println("rolling back after " + e.getClass().getSimpleName());
throw e;
}
}
public static void main(String[] args) {
try {
load("prices.csv");
} catch (IOException e) {
System.out.println("caller sees: " + e.getClass().getSimpleName());
}
}
}
Output:
rolling back after NoSuchFileException
caller sees: NoSuchFileException
Even though e is declared as Exception, the method only needs throws IOException, because the only checked exception the try block can produce is IOException. This lets one handler perform cleanup for every failure while keeping the method's declaration precise. The feature works only while the catch parameter is effectively final: assign anything to e inside the block and the compiler falls back to the declared type, reporting unreported exception Exception; must be caught or declared to be thrown.
When you need to rethrow as a different type — for example, converting a low-level SQLException into an application-level exception — pass the original as the cause so its information isn't lost. That technique is covered in the Exception Chaining lesson.