Exception Handling

throw & throws

15 min Lesson 5 of 14

throw & throws

In the previous lessons you learned how to catch exceptions that the JVM or the standard library raises. Sometimes, though, your own code needs to signal that something has gone wrong. That is what throw and throws are for. These two keywords look similar but serve very different roles — understanding both is essential for writing honest, self-documenting Java.

throw — raising an exception yourself

The throw keyword lets you create and fire an exception object at any point in your code. The moment Java hits a throw statement, execution of the current method stops and the exception starts travelling up the call stack looking for a matching catch block.

public class BankAccount { private double balance; public BankAccount(double initialBalance) { if (initialBalance < 0) { throw new IllegalArgumentException( "Initial balance cannot be negative: " + initialBalance ); } this.balance = initialBalance; } public void withdraw(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Amount must be positive."); } if (amount > balance) { throw new IllegalStateException( "Insufficient funds. Balance: " + balance + ", Requested: " + amount ); } balance -= amount; } }

Notice a few things:

  • throw takes an object — you always write throw new SomeException("message").
  • You choose the most specific exception class that fits the problem. IllegalArgumentException is right for bad inputs; IllegalStateException is right when the object itself is in the wrong state.
  • The message string should help the caller diagnose the issue — include the bad value where possible.
Why throw instead of returning a special value? A method that returns -1 or null to indicate failure forces every caller to remember to check — and they often forget. An exception cannot be silently ignored: if no one catches it, the program terminates with a visible stack trace.

throws — advertising checked exceptions

The throws keyword belongs on the method signature, not inside the body. It is a contract: it tells the compiler and every caller "this method might throw the listed checked exception, and you must deal with it."

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; public class FileProcessor { // The method declares it may throw IOException public String readFile(Path path) throws IOException { return Files.readString(path); // Files.readString itself throws IOException } }

Because IOException is a checked exception, the compiler forces every caller of readFile to either:

  1. Surround the call in a try-catch block, or
  2. Add the same throws IOException declaration to their own method, propagating the responsibility further up.
Unchecked exceptions do not require throws. RuntimeException and its subclasses (like IllegalArgumentException, NullPointerException) can be thrown anywhere without declaring them. Only checked exceptions (those that extend Exception but not RuntimeException) must be declared.

How the stack unwinds — propagation

When an exception is thrown and no matching catch is found in the current method, Java moves to the caller of that method and looks again. This continues up the chain until either a handler is found or the exception reaches the top of the stack (which crashes the thread).

public class PropagationDemo { public static void main(String[] args) { try { level1(); } catch (IllegalStateException e) { System.out.println("Caught in main: " + e.getMessage()); } } static void level1() { level2(); // no try-catch here — exception propagates up } static void level2() { level3(); // no try-catch here — exception propagates up } static void level3() { throw new IllegalStateException("Something went wrong deep down"); } }

Output:

Caught in main: Something went wrong deep down

level3 throws, level2 and level1 do not catch it, and it travels all the way to main where it is finally handled. The call stack unwinds automatically — no extra code needed in level1 or level2.

Combining throw and throws

A method that both declares a checked exception (with throws) and manually raises it (with throw) is very common:

public class UserService { public String findUser(int id) throws IllegalArgumentException { if (id <= 0) { throw new IllegalArgumentException("User ID must be positive, got: " + id); } // ... look up the user in a database return "Alice"; } }
Do not throw Exception or Throwable directly. Writing throw new Exception("something broke") forces callers to catch the broadest possible type, obscuring what actually went wrong. Always prefer a specific, meaningful exception class — use standard ones from the JDK or create your own (covered in Lesson 7).

A complete worked example

import java.io.IOException; import java.nio.file.Path; public class ReportGenerator { // declares checked IOException so callers must handle it public void generate(int reportId, Path outputDir) throws IOException { if (reportId <= 0) { // unchecked — no need to declare it in throws throw new IllegalArgumentException("reportId must be positive"); } if (!outputDir.toFile().isDirectory()) { throw new IOException("Output path is not a directory: " + outputDir); } // ... generate the report System.out.println("Report " + reportId + " written to " + outputDir); } public static void main(String[] args) { ReportGenerator gen = new ReportGenerator(); try { gen.generate(42, Path.of("/tmp/reports")); } catch (IOException e) { System.err.println("IO problem: " + e.getMessage()); } // IllegalArgumentException would propagate uncaught if we passed -1 } }

Summary

  • throw goes inside a method body — it fires an exception object right now.
  • throws goes on the method signature — it declares that the method might throw a checked exception and pushes the handling responsibility onto the caller.
  • Unchecked exceptions (RuntimeException subclasses) can be thrown freely without being declared.
  • When an exception is not caught locally, it propagates up the call stack automatically until something catches it or the program terminates.