Java throw Keyword

Su Suriya Ravichandran Updated 14 Nov 2026
11 min read ·Lesson 127 of 133

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");
PartMeaning
throwA 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:

  1. For 8 and 35, the if condition is false, so the method calculates and returns a price.
  2. For -4, the condition is true. new IllegalArgumentException(...) creates an exception object, and throw throws it.
  3. throw behaves like any exception: the method stops at that line — return is never reached — and the exception propagates to the caller. Nothing in main catches it, so the program ends with a stack trace, and "not reached" is never printed.
  4. The top stack frame, Ticket.price(Ticket.java:4), is the throw line. The JVM records the stack trace when the exception object is created (in Throwable'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.

ExceptionThrow it whenExample
IllegalArgumentExceptionAn argument has an invalid valueNegative quantity, malformed email
NullPointerExceptionAn argument that must not be null is nullObjects.requireNonNull(name, "name")
IllegalStateExceptionThe arguments are fine, but the object is in the wrong state for this callShipping an unpaid order, using a closed connection
IndexOutOfBoundsExceptionAn index is outside the valid rangeObjects.checkIndex(i, size)
UnsupportedOperationExceptionThe operation isn't supported by this implementationModifying a read-only view
ArithmeticExceptionAn arithmetic rule is brokenMath.addExact overflow
A custom exceptionA business rule needs its own type the caller can catch specificallyInsufficientFundsException (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) returns value if it isn't null, otherwise throws NullPointerException with your message. Without it, initials(null) would still fail at fullName.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 if 0 <= index < length, otherwise throws IndexOutOfBoundsException with a standard message.
  • throw in a switch expression. A switch expression must produce a value for every input. The default -> throw ... branch is allowed because a throw never 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:

  1. Constructor validation. "ORD-12" doesn't match the pattern ORD- followed by four digits, so the constructor throws IllegalArgumentException. When a constructor throws, the new expression never produces an object, so an invalid Order can never exist anywhere in the program.
  2. State checks. requireStatus centralises the rule "this action is only allowed in this status." It throws IllegalStateException, because the arguments are fine — it's the order's current state that forbids the action.
  3. A rule involving data. Paying is allowed in CREATED, but not with zero items, so pay() adds a second check.
  4. The state changes only after all checks pass. In pay(), status = Status.PAID is 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.
  5. 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.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Java throw Keyword and get answers drawn from it.

Signed-in readers only.