JAVA / EXCEPTIONS
throw, throws and signalling failure honestly
Raise failures with throw at the right moment, declare exactly which checked ones escape with throws, and stop hiding failure in return values.
What you will learn
- Throw an instance with new; control leaves the method immediately, no return needed
- Declare only the narrowest checked exceptions that can actually escape
- Reject bad arguments up front with IllegalArgumentException and the offending value
- Replace -1, null and false failure codes with a throw the caller cannot ignore
Understanding throw, throws and signalling failure honestly
The two keywords look alike and do unrelated jobs. throw is an executable statement that takes a single exception object, which is why it almost always reads throw new SomethingException(...); the instant it runs, the method stops producing a value and the JVM starts searching the call stack for a handler. throws is not code at all: it is a clause on the signature that tells the compiler which checked exception types are permitted to leave the method. Because a throw ends the method, any statement after it in the same block is unreachable and javac rejects it, and for the same reason a method may end with a throw where a return would otherwise be required.
Treat the throws clause as the part of the contract a caller reads before writing a single line against your method. It is enforced only for checked types; listing RuntimeException subclasses there changes nothing the compiler does and works purely as documentation. The clause is a ceiling, not a prediction: a method declared throws TimeoutException might never throw one, yet every caller must still catch it or re-declare it, so an over-broad clause imposes real work on people who will never see that failure. throws Exception is the extreme case, satisfying javac while telling callers only that something, somewhere, may go wrong.
Signalling honestly means the type and the timing of the throw match what the caller should do about it. Check arguments first and throw IllegalArgumentException, or IllegalStateException for a bad object state, with the rejected value in the message, so the failure surfaces before the method has done half its work or touched shared state. When the failure is an expected fact about the outside world such as a timeout, a missing file or a declined payment, choose a specific checked type and name it, which forces the caller to have a plan. What ruins all of this is encoding failure as -1, null, false or an empty list, because those values flow onward silently and the bug then appears far away from its cause.
The signature and the throw site are one design decision: the throw picks the type, the clause publishes it, and a caller who only reads the signature should already know what to plan for.
import java.util.concurrent.TimeoutException;
public class Main {
// The throws clause is a promise to callers: plan for a timeout.
// IllegalArgumentException is a caller bug, so it stays out of the clause.
static String fetch(String host, int timeoutMs) throws TimeoutException {
if (timeoutMs <= 0) {
throw new IllegalArgumentException("timeoutMs must be > 0, got " + timeoutMs);
}
if (host.equals("slow.example")) {
throw new TimeoutException("no answer from " + host + " in " + timeoutMs + "ms");
}
return "200 OK from " + host;
}
public static void main(String[] args) throws TimeoutException {
try {
System.out.println(fetch("fast.example", 500));
System.out.println(fetch("slow.example", 500));
System.out.println("this line is skipped: the throw left fetch and the try block");
} catch (TimeoutException e) {
System.out.println("handled: " + e.getMessage());
}
try {
fetch("fast.example", 0);
} catch (IllegalArgumentException e) {
System.out.println("refused before any work: " + e.getMessage());
}
System.out.println(fetch("fast.example", 500));
}
}throw performs a failure at runtime while throws publishes it in the signature, and both should name the failure precisely instead of letting a caller mistake it for success.
Worked examples
throws Exception versus a named failure
Shows what a caller loses when the clause is broad, even though the thrown object is just as specific.
import java.util.concurrent.TimeoutException;
public class Main {
// Vague: the signature says anything may fail, so callers learn nothing.
static String vagueFetch(String host) throws Exception {
throw new TimeoutException(host);
}
// Precise: the one recoverable failure mode is named.
static String preciseFetch(String host) throws TimeoutException {
throw new TimeoutException(host);
}
public static void main(String[] args) {
try {
vagueFetch("a.example");
} catch (Exception e) {
System.out.println("vague: signature said Exception, runtime brought "
+ e.getClass().getSimpleName());
}
try {
preciseFetch("b.example");
} catch (TimeoutException e) {
System.out.println("precise: timeout talking to " + e.getMessage() + ", retrying");
}
}
}Example explained
Line 1vagueFetch declares throws Exception, so main has no option but to handle Exception itself.
Line 2getSimpleName() shows the thrown object was always a TimeoutException; only the declaration was imprecise.
Line 3preciseFetch names TimeoutException, so the catch clause is itself the decision that this failure is retryable.
Line 4Neither method needs a return statement, because the throw makes the end of the body unreachable.
Overrides may narrow a throws clause
Demonstrates that the clause is checked against the compile-time type, not against what actually happens.
import java.util.concurrent.TimeoutException;
public class Main {
static class Source {
String read() throws TimeoutException {
throw new TimeoutException("network stalled");
}
}
static class Cache extends Source {
@Override
String read() { // narrower contract: it cannot time out
return "cached value";
}
}
public static void main(String[] args) {
Cache cache = new Cache();
System.out.println(cache.read()); // no handling required at all
Source source = cache; // same object, wider declared contract
try {
System.out.println(source.read());
} catch (TimeoutException e) {
System.out.println("would have handled: " + e.getMessage());
}
}
}Example explained
Line 1Cache.read drops the clause, which is legal: an override may declare fewer or narrower checked exceptions, never more.
Line 2cache.read() compiles without a try because the static type Cache declares nothing that can escape.
Line 3source.read() calls the very same Cache.read at runtime, yet javac still demands handling because the reference type is Source.
Line 4The catch block never runs, which is the plain evidence that throws limits what may escape rather than promising it will.
Important notes
The throws clause is checked against the compile-time type of the reference you call through, so an override that drops the clause only frees callers who hold the subclass type.
throw null compiles, but at runtime it produces a NullPointerException instead of the failure you meant, so never throw an exception variable that might be null.
Common mistakes
Swapping the keywords: writing throws new IOException("...") inside a body, or throw IOException in a signature. Neither compiles, and the error text talks about syntax rather than about the real confusion.
Bolting throws Exception onto a method to silence the compiler. Every caller is now pushed into handling Exception, and any new failure mode you add later slips through because the signature never changes.
Catching the failure, printing it, and returning -1 or null anyway. The method reports success to its caller, the wrong value spreads, and the actual error exists only in a log line.
Try it yourself
Change, predict, then run
Take a method static int daysInMonth(int month) that currently returns -1 for anything outside 1 to 12 and change it to throw an IllegalArgumentException whose message contains the rejected number. Call it with 2 and then with 13 from main, and confirm that the print statement after the failing call never runs.
Open the Java workspaceCheck your understanding
A method is declared static String load(String key) throws Exception, but its body can only ever throw TimeoutException. What does the vague clause actually cost the caller?
- Callers are forced to handle Exception itself, and the signature no longer tells them a timeout is the failure worth planning for.
- Nothing: javac infers the exceptions the body really throws and narrows the clause for callers.
- A catch (TimeoutException e) block around the call becomes dead code that javac rejects.
- The TimeoutException is turned into an unchecked exception, since Exception is a supertype of RuntimeException.
Show answer
Callers compile against the clause, not the body, so every call site must catch or re-declare Exception and gets no hint that a retryable timeout is the real risk. Option 3 is tempting because the block never executes, but catching a subtype of a declared checked exception is perfectly legal; the problem is that nothing in the signature asks for it, and Exception must still be handled anyway.