Java throws Keyword
What throws Declares
The throws keyword appears in a method or constructor declaration. It lists the checked exceptions that the method may pass on to its caller instead of handling them itself.
returnType methodName(parameters) throws ExceptionType1, ExceptionType2 {
// body
}
static String readConfig(Path file) throws IOException {
return Files.readString(file); // may throw IOException; we don't catch it here
}
throws IOException is part of the method's contract, just like its parameter and return types. It tells the compiler and every caller: "calling this method can fail with an IOException, and you must be prepared for that."
Why Java has it: for checked exceptions, the compiler enforces the catch-or-declare rule. Every checked exception that can occur in a method must be either caught there (with try...catch) or declared with throws. Declaring it moves the responsibility one level up. Each caller then faces the same choice — catch it or declare it — until some method decides to handle it. This guarantees that a checked exception can never be forgotten silently.
throw vs throws
The two keywords look alike but do completely different things:
throw | throws | |
|---|---|---|
| What it is | A statement | A clause in a method/constructor signature |
| Where it appears | Inside a method body | After the parameter list |
| What follows it | One exception object: throw new IOException("...") | One or more exception types: throws IOException, SQLException |
| What it does | Actually throws an exception right now | Declares that the method might let these exceptions escape |
| Needed for | Signalling an error | Checked exceptions that the method doesn't catch |
A method can declare throws IOException without containing any throw statement — the exception may come from a method it calls, as in readConfig above. And a method can throw an unchecked exception without any throws clause.
Propagating Through Application Layers
Real applications are organised in layers, and usually only the top layer knows how a failure should be presented. throws lets an exception travel from where it happens to where it can be handled sensibly.
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
public class Layers {
// Data layer: knows how to read files, not what to do when they are missing
static List<String> readLines(Path file) throws IOException {
return Files.readAllLines(file);
}
// Business layer: computes a total, still can't decide how to report a missing file
static double totalSales(Path file) throws IOException {
double total = 0;
for (String line : readLines(file)) {
total += Double.parseDouble(line.split(",")[1]);
}
return total;
}
// Presentation layer: decides what the user sees
static void showReport(Path file) {
try {
System.out.printf("%s: total sales %.2f%n", file.getFileName(), totalSales(file));
} catch (IOException e) {
System.out.println(file.getFileName() + ": report unavailable (" + e.getClass().getSimpleName() + ")");
}
}
public static void main(String[] args) throws IOException {
Path dir = Files.createTempDirectory("sales");
Path nov = Files.writeString(dir.resolve("nov.csv"), "pen,1250.50\nbag,3400.00\nink,980.25\n");
showReport(nov);
showReport(dir.resolve("dec.csv"));
}
}
Output:
nov.csv: total sales 5630.75
dec.csv: report unavailable (NoSuchFileException)
How the exception travels for dec.csv:
Files.readAllLinesis declaredthrows IOException. The file doesn't exist, so it throwsNoSuchFileException(a subclass ofIOException).readLinesdoesn't catch it. Because its signature saysthrows IOException, this compiles, and the exception passes tototalSales.totalSalesalso declaresthrows IOException, so the exception passes on toshowReport. The loop never runs.showReportcallstotalSalesinsidetry...catch (IOException e). Here the chain ends: the user sees a short message and the program continues with the next report.
If you delete throws IOException from totalSales, compilation fails with unreported exception IOException; must be caught or declared to be thrown at the call to readLines. The compiler checks every link in the chain.
showReport has no throws clause because it handles the exception. main declares throws IOException because createTempDirectory and writeString can fail during setup, and for a small demo it's acceptable to let such a failure end the program.
Declaring Several Exceptions
List multiple types separated by commas:
void syncInventory() throws IOException, InterruptedException {
String data = Files.readString(Path.of("stock.csv")); // IOException
Thread.sleep(500); // InterruptedException
// ...
}
A caller can catch them separately, with a multi-catch, or declare them in turn. You only need to declare checked exceptions. If you declare a parent type such as throws Exception, it covers every checked exception, but callers then have no idea what can actually go wrong and are forced to handle Exception in general. Declare the most specific types that are meaningful.
main with throws
import java.nio.file.*;
public class MainThrows {
public static void main(String[] args) throws Exception {
System.out.println(Files.readString(Path.of("app.yaml")));
}
}
Output (when app.yaml doesn't exist):
Exception in thread "main" java.nio.file.NoSuchFileException: app.yaml
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
...
at java.base/java.nio.file.Files.readString(Files.java:3318)
at MainThrows.main(MainThrows.java:5)
main can declare exceptions like any method. There's no caller in your code above it, so an exception leaving main reaches the JVM, which prints the stack trace and exits with status 1. The java.base/... frames show where inside the JDK the failure originated; the last frame is your line. This is fine for quick scripts and learning examples, but in real applications main should catch failures and print a clear message (and often choose an exit code with System.exit).
throws Clauses and Unchecked Exceptions
You may list unchecked exceptions in a throws clause:
/**
* Withdraws money from the account.
*
* @throws IllegalArgumentException if amount is not positive
* @throws IllegalStateException if the account is frozen
*/
void withdraw(long amount) throws IllegalArgumentException, IllegalStateException { ... }
The compiler ignores unchecked types in throws — callers are not forced to handle them. The convention in the JDK and most libraries is to document unchecked exceptions with the Javadoc @throws tag and leave them out of the throws clause, keeping the clause for checked exceptions the caller must deal with.
throws on Constructors
Constructors can declare checked exceptions too. If a constructor throws, no object is created.
import java.io.*;
import java.nio.file.*;
import java.util.*;
public class Ctor {
static class PriceList {
private final Map<String, Double> prices = new HashMap<>();
PriceList(Path file) throws IOException {
for (String line : Files.readAllLines(file)) {
String[] f = line.split("=");
prices.put(f[0], Double.parseDouble(f[1]));
}
}
double price(String sku) { return prices.getOrDefault(sku, 0.0); }
}
public static void main(String[] args) throws IOException {
Path f = Files.writeString(Files.createTempFile("prices", ".txt"), "PEN=12.5\nINK=80\n");
PriceList list = new PriceList(f);
System.out.println(list.price("INK"));
}
}
Output:
80.0
Every new PriceList(...) expression is subject to the catch-or-declare rule, exactly like a method call. Constructors that do I/O are generally best avoided — a static factory method such as PriceList.load(path) makes the possible failure more visible — but the rule is the same either way.
throws and Method Overriding
When a subclass overrides a method, it cannot declare new or broader checked exceptions than the method it overrides. The reason is substitutability: code written against the parent type has only prepared for the exceptions the parent declares.
import java.io.IOException;
public class Override1 {
static class Exporter {
void export(String data) { System.out.println(data); }
}
static class FileExporter extends Exporter {
@Override
void export(String data) throws IOException {
throw new IOException("disk full");
}
}
}
Compiler output:
Override1.java:10: error: export(String) in FileExporter cannot override export(String) in Exporter
void export(String data) throws IOException {
^
overridden method does not throw IOException
1 error
Code holding an Exporter reference calls export without any try, because Exporter.export declares nothing. If a FileExporter were allowed to throw IOException there, that code would receive a checked exception it was never forced to handle.
What is allowed:
| Overriding method declares | Allowed? |
|---|---|
| The same checked exceptions | Yes |
| Fewer checked exceptions, or none | Yes |
| Subclasses of the declared exceptions (narrower) | Yes |
| New or broader checked exceptions | No |
| Any unchecked exceptions | Yes — the rule applies only to checked ones |
So the design decision belongs to the parent type. If implementations will reasonably need to do I/O, the interface itself should declare it:
import java.io.*;
import java.nio.file.*;
public class Override2 {
interface Exporter {
void export(String data) throws IOException;
}
static class ConsoleExporter implements Exporter {
@Override
public void export(String data) { // fewer exceptions: allowed
System.out.println("console: " + data);
}
}
static class FileExporter implements Exporter {
private final Path target;
FileExporter(Path target) { this.target = target; }
@Override
public void export(String data) throws NoSuchFileException { // narrower subclass: allowed
if (!Files.exists(target.getParent())) {
throw new NoSuchFileException(target.getParent().toString());
}
System.out.println("file: " + data);
}
}
public static void main(String[] args) {
Exporter[] exporters = { new ConsoleExporter(), new FileExporter(Path.of("/no/such/dir/out.txt")) };
for (Exporter ex : exporters) {
try {
ex.export("Q3 sales");
} catch (IOException e) {
System.out.println("export failed: " + e);
}
}
new ConsoleExporter().export("no try needed");
}
}
Output:
console: Q3 sales
export failed: java.nio.file.NoSuchFileException: /no/such/dir
console: no try needed
- Through the
Exporterinterface, the compiler only seesthrows IOException, so the loop must handleIOException— and that handler correctly catches theNoSuchFileExceptionfromFileExporter. - The last line calls
exporton a variable of typeConsoleExporter, whose method declares nothing. The compiler checks the static type of the reference, so notryis required there.
The JDK uses this same pattern: Runnable.run() declares no exceptions, while Callable.call() declares throws Exception, which is why a Callable passed to an ExecutorService can throw checked exceptions and a Runnable can't.
Checked Exceptions and Lambdas
The standard functional interfaces (Function, Consumer, Supplier, Predicate) don't declare any checked exceptions. Therefore a lambda implementing them can't throw one:
import java.nio.file.*;
import java.util.*;
public class Lambda1 {
public static void main(String[] args) {
List<Path> files = List.of(Path.of("a.txt"));
files.stream().map(p -> Files.readString(p)).forEach(System.out::println);
}
}
Compiler output:
Lambda1.java:7: error: unreported exception IOException; must be caught or declared to be thrown
files.stream().map(p -> Files.readString(p)).forEach(System.out::println);
^
1 error
Adding throws IOException to main doesn't help, because the exception would escape from the lambda body, which is the implementation of Function.apply — and apply declares no exceptions (the overriding rule again).
The usual solution is to catch the checked exception inside a helper and rethrow it wrapped in an unchecked exception. The JDK provides java.io.UncheckedIOException for exactly this:
import java.io.*;
import java.nio.file.*;
import java.util.*;
public class Lambda2 {
static String readUnchecked(Path p) {
try {
return Files.readString(p);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static void main(String[] args) throws IOException {
Path dir = Files.createTempDirectory("notes");
Path a = Files.writeString(dir.resolve("a.txt"), "first note");
List<Path> files = List.of(a, dir.resolve("b.txt"));
try {
files.stream().map(Lambda2::readUnchecked).forEach(System.out::println);
} catch (UncheckedIOException e) {
IOException cause = e.getCause();
System.out.println("stream stopped: " + cause.getClass().getSimpleName());
}
}
}
Output:
first note
stream stopped: NoSuchFileException
readUncheckedhas nothrowsclause, so it can be used as aFunction<Path, String>via the method referenceLambda2::readUnchecked.new UncheckedIOException(e)keeps the original exception as its cause, so no information is lost.getCause()onUncheckedIOExceptionis declared to returnIOException, which is why no cast is needed.- The first file is printed; the second fails and the stream stops. The caller catches the unchecked wrapper and inspects the cause.
Wrapping and causes are explored further in the Exception Chaining lesson, and when to choose checked versus unchecked exceptions is covered in the next two lessons.
Deciding Between Catching and Declaring
| Situation | Choice |
|---|---|
| The method can fully recover (default value, retry, skip one record) | Catch it |
| The method can't decide what the right response is; a caller can | Declare it with throws |
The exception is a low-level detail that callers shouldn't depend on (e.g. a repository throwing SQLException) | Catch it and throw an application-level exception with the original as the cause |
| Inside a lambda for the standard functional interfaces | Catch it, or wrap it in an unchecked exception |
The method is an entry point (request handler, scheduled job, main of a real app) | Catch it, log it and report the failure |
A good rule of thumb: handle an exception at the lowest level that has enough context to make a correct decision, and let it propagate with throws until it reaches that level.