Java throw Keyword
Java throw Keyword
throw is the statement that creates and dispatches an exception at the exact point where something goes wrong. Write throw new IllegalArgumentException("amount must be positive"), and execution stops at that line — the rest of the method body does not run, and the JVM begins searching for a catch block that can handle this exception. This is the only mechanism in Java for explicitly signalling that something has failed in a way the normal return value cannot express.
What Does throw Do?
throw takes a single operand — any object whose type is Throwable or a subclass — and raises it as an exception. The statement has three effects, all immediate: it stops execution of the current method at that line, it transfers control to the nearest enclosing catch block whose type matches, and if no such block exists in the current method, it propagates the exception to the caller.
throw new IllegalArgumentException("Invalid input");
| | |
| | +-- constructor argument: the message
| +-- creates a new exception OBJECT (an instance of Throwable)
+-- the THROW statement: raises that object as an exception RIGHT NOW
WHAT throw REQUIRES:
The operand must be an object of type Throwable or a subclass.
throw "some string"; ← COMPILE ERROR: String is not Throwable
throw new RuntimeException(); ← VALID: RuntimeException extends Throwable
throw exceptionVariable; ← VALID: rethrowing an existing exception object
WHERE throw CAN APPEAR:
Inside any method body, constructor, or static/instance initializer block.
Inside a lambda body (with restrictions on checked exceptions — see FAQs).
Inside a catch block (rethrowing the same or a different exception).
Inside a finally block (with caution — see Common Mistakes).
EXECUTION STOPS IMMEDIATELY:
void validate(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
// Any code here is unreachable — compiler flags it
}
System.out.println("Valid: " + amount); // skipped if throw executed above
}
Syntax — All Forms of throw
throw appears in several distinct patterns, each used for a different purpose. Recognising these patterns is what separates code that communicates failure clearly from code that hides it.
FORM 1 — Throw a new unchecked exception:
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative: " + price);
}
FORM 2 — Throw a new checked exception (method must declare throws):
if (!file.exists()) {
throw new FileNotFoundException("Config file missing: " + path);
}
// The enclosing method needs: throws FileNotFoundException
FORM 3 — Throw a custom exception:
if (balance < amount) {
throw new InsufficientFundsException(accountId, amount, balance);
}
FORM 4 — Rethrow the SAME exception object (after logging, for example):
try {
operation();
} catch (IOException ioException) {
logger.warn("IO failure, propagating", ioException);
throw ioException; // same object — same stack trace, same type
}
FORM 5 — Throw a NEW exception, wrapping the original as the cause:
try {
operation();
} catch (SQLException sqlException) {
throw new ServiceException("Operation failed", sqlException);
// NEW exception object — but cause chain links back to sqlException
}
FORM 6 — Throw inside a ternary or single-expression context (Java 14+ switch):
String category = switch (status) {
case "ACTIVE", "PENDING" -> "OPEN";
case "CLOSED", "CANCELLED" -> "CLOSED";
default -> throw new IllegalStateException("Unknown status: " + status);
};
THROW vs THROWS — THE MOST COMMON CONFUSION:
throw — a STATEMENT, executed at runtime, raises one specific exception object
public void validate() {
throw new IllegalArgumentException("bad input"); // ACTION
}
throws — a DECLARATION, part of a method signature, lists possible checked exceptions
public void readFile() throws IOException { // CONTRACT
// may or may not actually throw — just declares the possibility
}
A method can have throws in its signature WITHOUT ever executing throw
(if the checked exception comes from a method it calls).
A method can execute throw WITHOUT throws in its signature
(if the exception is unchecked).
How throw Works Internally
When throw executes, the JVM does not search for a handler within the current method first and then decide — it immediately begins the standard exception dispatch mechanism: consult the current method's exception table, and if no entry matches, pop the stack frame and repeat in the caller.
EXECUTION SEQUENCE FOR throw new SomeException("message"):
STEP 1 — Object construction:
new SomeException("message")
Allocates a SomeException object on the heap.
Constructor runs — including Throwable's constructor, which captures
the current thread's call stack into the stackTrace field.
THIS is the expensive part — walking and recording every active frame.
STEP 2 — The throw statement executes:
The constructed object becomes the "current exception" for this thread.
Normal execution of the current method STOPS at this line.
STEP 3 — Exception table lookup (current method):
JVM checks: does this method have a try block covering this program
counter, with a catch clause matching SomeException (via instanceof)?
YES → jump to that catch handler — DONE, throw is fully resolved here.
NO → proceed to step 4.
STEP 4 — Stack unwinding:
Current method's stack frame is POPPED (local variables discarded).
The calling method becomes "current" — repeat step 3 for that method.
Continues until a handler is found, or the stack is exhausted.
STEP 5 — No handler found anywhere:
Thread's UncaughtExceptionHandler runs.
Default: print stack trace to stderr, terminate the thread.
THE STACK TRACE WAS CAPTURED AT STEP 1 — not at step 5.
This is why the stack trace shows where the exception was CREATED
(the throw site), not where it was eventually caught or printed.
Throwing Unchecked Exceptions
Unchecked exceptions — subclasses of RuntimeException — are the most common targets of throw. They require no throws declaration and typically signal that the caller passed invalid input or used the API incorrectly.
1// File: ThrowUncheckedDemo.java
2
3import java.util.List;
4
5public class ThrowUncheckedDemo {
6
7 // throw IllegalArgumentException — caller passed an invalid argument
8 static double calculateDiscount(double price, double discountPercent) {
9 if (price < 0) {
10 throw new IllegalArgumentException(
11 "Price cannot be negative: " + price);
12 }
13 if (discountPercent < 0 || discountPercent > 100) {
14 throw new IllegalArgumentException(
15 "Discount must be between 0 and 100, got: " + discountPercent);
16 }
17 return price * (1 - discountPercent / 100.0);
18 }
19
20 // throw IllegalStateException — object used in the wrong lifecycle state
21 static class ReportBuilder {
22 private boolean finalized = false;
23 private final StringBuilder content = new StringBuilder();
24
25 void addLine(String line) {
26 if (finalized) {
27 throw new IllegalStateException(
28 "Cannot add content after finalize() was called");
29 }
30 content.append(line).append("\n");
31 }
32
33 String finalizeReport() {
34 finalized = true;
35 return content.toString();
36 }
37 }
38
39 // throw NullPointerException explicitly — fail fast on missing input
40 // (more readable than relying on the implicit NPE that would happen later)
41 static int countOrders(List<String> orderIds) {
42 if (orderIds == null) {
43 throw new NullPointerException("orderIds list must not be null");
44 }
45 return orderIds.size();
46 }
47
48 public static void main(String[] args) {
49
50 System.out.println("=== IllegalArgumentException — invalid arguments ===");
51 System.out.printf("Rs.1299 with 20%% off: Rs.%.2f%n",
52 calculateDiscount(1299.0, 20));
53
54 try {
55 calculateDiscount(-100, 20);
56 } catch (IllegalArgumentException iae) {
57 System.out.println("Caught: " + iae.getMessage());
58 }
59
60 try {
61 calculateDiscount(500, 150);
62 } catch (IllegalArgumentException iae) {
63 System.out.println("Caught: " + iae.getMessage());
64 }
65
66 System.out.println();
67
68 System.out.println("=== IllegalStateException — wrong lifecycle order ===");
69 ReportBuilder report = new ReportBuilder();
70 report.addLine("Daily sales: Rs.45,000");
71 report.addLine("Top product: Wireless Earbuds");
72 System.out.println(report.finalizeReport());
73
74 try {
75 report.addLine("Late addition"); // after finalizeReport()
76 } catch (IllegalStateException ise) {
77 System.out.println("Caught: " + ise.getMessage());
78 }
79
80 System.out.println();
81
82 System.out.println("=== Explicit NullPointerException — fail fast ===");
83 System.out.println("Count: " + countOrders(List.of("ORD-001", "ORD-002")));
84 try {
85 countOrders(null);
86 } catch (NullPointerException npe) {
87 System.out.println("Caught: " + npe.getMessage());
88 }
89 }
90}Output:
=== IllegalArgumentException — invalid arguments ===
Rs.1299 with 20% off: Rs.1039.20
Caught: Price cannot be negative: -100.0
Caught: Discount must be between 0 and 100, got: 150.0
=== IllegalStateException — wrong lifecycle order ===
Daily sales: Rs.45,000
Top product: Wireless Earbuds
Caught: Cannot add content after finalize() was called
=== Explicit NullPointerException — fail fast ===
Count: 2
Caught: orderIds list must not be null
Throwing Checked Exceptions
Throwing a checked exception requires the enclosing method to declare it with throws — otherwise the code does not compile. This is the visible contract that tells callers "this operation can fail in this specific, expected way."
1// File: ThrowCheckedDemo.java
2
3import java.io.FileNotFoundException;
4import java.io.IOException;
5
6public class ThrowCheckedDemo {
7
8 // throw FileNotFoundException — must declare throws IOException
9 // (FileNotFoundException IS-A IOException, so declaring IOException covers it)
10 static String loadTemplate(String templateName) throws IOException {
11 if (templateName == null || templateName.isBlank()) {
12 // Unchecked — no throws needed for IllegalArgumentException
13 throw new IllegalArgumentException("Template name required");
14 }
15 if (!templateName.endsWith(".html")) {
16 // Checked — REQUIRES throws IOException on this method
17 throw new FileNotFoundException(
18 "Template must be .html, got: " + templateName);
19 }
20 return "<html>template-content-for-" + templateName + "</html>";
21 }
22
23 // Method that propagates the checked exception further (no catch here)
24 static String renderPage(String templateName) throws IOException {
25 // Calling a method that throws IOException requires:
26 // catch it here, OR declare throws IOException — chosen: propagate
27 String template = loadTemplate(templateName);
28 return template.replace("{{title}}", "Dashboard");
29 }
30
31 // Method that handles the checked exception — no throws needed
32 static String renderPageSafe(String templateName) {
33 try {
34 return renderPage(templateName);
35 } catch (IOException ioException) {
36 // Handled here — caller of renderPageSafe does not need throws
37 return "<html>Default Template</html>";
38 }
39 }
40
41 public static void main(String[] args) {
42
43 System.out.println("=== Propagation: caller must catch or declare ===");
44 try {
45 System.out.println(renderPage("dashboard.html"));
46 System.out.println(renderPage("dashboard.txt")); // wrong extension
47 } catch (IOException ioException) {
48 System.out.println("Caught at top level: " + ioException.getMessage());
49 }
50
51 System.out.println();
52
53 System.out.println("=== Handled internally — no propagation ===");
54 System.out.println(renderPageSafe("dashboard.html"));
55 System.out.println(renderPageSafe("dashboard.txt")); // falls back to default
56
57 System.out.println();
58
59 System.out.println("=== Unchecked exception — no throws declaration needed ===");
60 try {
61 loadTemplate(null);
62 } catch (IllegalArgumentException iae) {
63 System.out.println("Caught: " + iae.getMessage());
64 }
65 }
66}Output:
=== Propagation: caller must catch or declare ===
<html>template-content-for-dashboard.html</html>
Caught at top level: Template must be .html, got: dashboard.txt
=== Handled internally — no propagation ===
<html>template-content-for-dashboard.html</html>
<html>Default Template</html>
=== Unchecked exception — no throws declaration needed ===
Caught: Template name required
Rethrowing and Wrapping
Two distinct patterns appear when an exception is caught and a throw follows. Rethrowing the same object preserves the exact type and stack trace. Wrapping creates a new exception with the original attached as the cause — adding context while preserving the diagnostic chain.
1// File: RethrowWrapDemo.java
2
3import java.io.IOException;
4import java.sql.SQLException;
5
6public class RethrowWrapDemo {
7
8 static class OrderProcessingException extends RuntimeException {
9 OrderProcessingException(String message, Throwable cause) {
10 super(message, cause); // cause argument links the chain
11 }
12 }
13
14 // PATTERN 1 — Rethrow the SAME exception after side-effect (logging)
15 static void saveWithLogging(String orderId) throws IOException {
16 try {
17 writeOrder(orderId);
18 } catch (IOException ioException) {
19 System.out.println(" LOG: Write failed for " + orderId +
20 " — " + ioException.getMessage());
21 throw ioException; // SAME object — type and stack trace preserved
22 }
23 }
24
25 // PATTERN 2 — Wrap in a NEW exception, preserving the cause
26 static void saveWithTranslation(String orderId) {
27 try {
28 writeOrder(orderId);
29 } catch (IOException ioException) {
30 // NEW OrderProcessingException — but cause chain links to ioException
31 throw new OrderProcessingException(
32 "Failed to save order: " + orderId, ioException);
33 }
34 }
35
36 // PATTERN 3 — Catch multiple types, wrap uniformly
37 static void saveWithUnifiedHandling(String orderId, String mode) {
38 try {
39 if (mode.equals("FILE")) writeOrder(orderId);
40 else queryDatabase(orderId);
41 } catch (IOException | SQLException infrastructureException) {
42 throw new OrderProcessingException(
43 "Infrastructure failure for order: " + orderId, infrastructureException);
44 }
45 }
46
47 static void writeOrder(String orderId) throws IOException {
48 if (orderId.startsWith("FAIL")) {
49 throw new IOException("Disk write failed for: " + orderId);
50 }
51 System.out.println(" Order written: " + orderId);
52 }
53
54 static void queryDatabase(String orderId) throws SQLException {
55 if (orderId.startsWith("FAIL")) {
56 throw new SQLException("DB connection lost for: " + orderId);
57 }
58 System.out.println(" Order queried: " + orderId);
59 }
60
61 public static void main(String[] args) {
62
63 System.out.println("=== Pattern 1: rethrow same exception ===");
64 try {
65 saveWithLogging("ORD-001"); // success
66 saveWithLogging("FAIL-ORD-002"); // throws, logged, rethrown
67 } catch (IOException ioException) {
68 System.out.println("Caller caught: [" +
69 ioException.getClass().getSimpleName() + "] " + ioException.getMessage());
70 }
71
72 System.out.println();
73
74 System.out.println("=== Pattern 2: wrap with cause preserved ===");
75 try {
76 saveWithTranslation("FAIL-ORD-003");
77 } catch (OrderProcessingException ope) {
78 System.out.println("Caller caught: " + ope.getMessage());
79 System.out.println("Root cause: [" +
80 ope.getCause().getClass().getSimpleName() + "] " + ope.getCause().getMessage());
81 }
82
83 System.out.println();
84
85 System.out.println("=== Pattern 3: multi-catch wrapped uniformly ===");
86 try {
87 saveWithUnifiedHandling("FAIL-ORD-004", "FILE");
88 } catch (OrderProcessingException ope) {
89 System.out.println("Caller caught [via " +
90 ope.getCause().getClass().getSimpleName() + "]: " + ope.getMessage());
91 }
92 try {
93 saveWithUnifiedHandling("FAIL-ORD-005", "DB");
94 } catch (OrderProcessingException ope) {
95 System.out.println("Caller caught [via " +
96 ope.getCause().getClass().getSimpleName() + "]: " + ope.getMessage());
97 }
98 }
99}Output:
=== Pattern 1: rethrow same exception ===
Order written: ORD-001
LOG: Write failed for FAIL-ORD-002 — Disk write failed for: FAIL-ORD-002
Caller caught: [IOException] Disk write failed for: FAIL-ORD-002
=== Pattern 2: wrap with cause preserved ===
Caller caught: Failed to save order: FAIL-ORD-003
Root cause: [IOException] Disk write failed for: FAIL-ORD-003
=== Pattern 3: multi-catch wrapped uniformly ===
Caller caught [via IOException]: Infrastructure failure for order: FAIL-ORD-004
Caller caught [via SQLException]: Infrastructure failure for order: FAIL-ORD-005
Real-World Example — PhonePe Spending Limit Validator
A wallet transaction validator at PhonePe checks every debit request against multiple rules before allowing it through. Each rule violation throws a specific exception — some checked (the caller has a defined recovery path), some unchecked (programming-level contract violations on the request object itself). The throw site is always the precise point where the rule fails, with a message carrying the exact numbers involved.
1// File: TransactionRequest.java
2
3public record TransactionRequest(
4 String walletId,
5 double amount,
6 String merchantId,
7 String transactionType) {}1// File: SpendingLimitExceededException.java
2
3// Checked — caller has a defined recovery path (suggest EMI, partial payment)
4public class SpendingLimitExceededException extends Exception {
5
6 private final double requestedAmount;
7 private final double remainingLimit;
8
9 public SpendingLimitExceededException(
10 double requestedAmount, double remainingLimit) {
11 super(String.format(
12 "Requested Rs.%.2f exceeds remaining daily limit of Rs.%.2f",
13 requestedAmount, remainingLimit));
14 this.requestedAmount = requestedAmount;
15 this.remainingLimit = remainingLimit;
16 }
17
18 public double getRequestedAmount() { return requestedAmount; }
19 public double getRemainingLimit() { return remainingLimit; }
20}1// File: TransactionValidator.java
2
3public class TransactionValidator {
4
5 private static final double DAILY_LIMIT = 100_000.0;
6
7 // Throws unchecked IllegalArgumentException — request object is malformed
8 // This is a contract violation, not a business condition — fix the caller
9 private void validateRequestShape(TransactionRequest request) {
10 if (request == null) {
11 throw new IllegalArgumentException("TransactionRequest must not be null");
12 }
13 if (request.walletId() == null || request.walletId().isBlank()) {
14 throw new IllegalArgumentException("walletId is required");
15 }
16 if (request.amount() <= 0) {
17 throw new IllegalArgumentException(
18 "amount must be positive, got: " + request.amount());
19 }
20 }
21
22 // Throws unchecked IllegalStateException — wallet must be active for any debit
23 private void validateWalletStatus(String walletId, boolean isActive) {
24 if (!isActive) {
25 throw new IllegalStateException(
26 "Wallet " + walletId + " is not active — cannot process transaction");
27 }
28 }
29
30 // Throws CHECKED SpendingLimitExceededException — caller has a recovery path
31 private void validateSpendingLimit(double amount, double alreadySpentToday)
32 throws SpendingLimitExceededException {
33 double remaining = DAILY_LIMIT - alreadySpentToday;
34 if (amount > remaining) {
35 throw new SpendingLimitExceededException(amount, remaining);
36 }
37 }
38
39 // Throws unchecked custom exception — fraud detection is a hard stop, no recovery
40 private void validateFraudCheck(TransactionRequest request) {
41 if (request.merchantId().startsWith("BLOCKED")) {
42 throw new SecurityException(
43 "Merchant " + request.merchantId() + " is on the fraud blocklist");
44 }
45 }
46
47 public String processTransaction(
48 TransactionRequest request, boolean walletActive, double alreadySpentToday)
49 throws SpendingLimitExceededException {
50
51 validateRequestShape(request);
52 validateWalletStatus(request.walletId(), walletActive);
53 validateFraudCheck(request);
54 validateSpendingLimit(request.amount(), alreadySpentToday);
55
56 return String.format("APPROVED: %s debited Rs.%.2f to %s",
57 request.walletId(), request.amount(), request.merchantId());
58 }
59
60 public static void main(String[] args) {
61 TransactionValidator validator = new TransactionValidator();
62
63 System.out.println("=== Valid transaction ===");
64 try {
65 System.out.println(validator.processTransaction(
66 new TransactionRequest("W-1001", 5000.0, "M-Swiggy", "DEBIT"),
67 true, 20000.0));
68 } catch (SpendingLimitExceededException slee) {
69 System.out.println("Limit exceeded: " + slee.getMessage());
70 }
71
72 System.out.println();
73
74 System.out.println("=== Inactive wallet — IllegalStateException ===");
75 try {
76 validator.processTransaction(
77 new TransactionRequest("W-1002", 500.0, "M-Zomato", "DEBIT"),
78 false, 0.0);
79 } catch (IllegalStateException ise) {
80 System.out.println("Caught: " + ise.getMessage());
81 } catch (SpendingLimitExceededException slee) {
82 System.out.println("Limit exceeded: " + slee.getMessage());
83 }
84
85 System.out.println();
86
87 System.out.println("=== Fraud blocklist — SecurityException ===");
88 try {
89 validator.processTransaction(
90 new TransactionRequest("W-1003", 200.0, "BLOCKED-MERCHANT", "DEBIT"),
91 true, 0.0);
92 } catch (SecurityException se) {
93 System.out.println("Caught: " + se.getMessage());
94 } catch (SpendingLimitExceededException slee) {
95 System.out.println("Limit exceeded: " + slee.getMessage());
96 }
97
98 System.out.println();
99
100 System.out.println("=== Daily limit exceeded — checked exception ===");
101 try {
102 validator.processTransaction(
103 new TransactionRequest("W-1004", 30000.0, "M-Amazon", "DEBIT"),
104 true, 80000.0);
105 } catch (SpendingLimitExceededException slee) {
106 System.out.printf("Limit exceeded: requested Rs.%.2f, remaining Rs.%.2f%n",
107 slee.getRequestedAmount(), slee.getRemainingLimit());
108 System.out.println("Action: suggest splitting into multiple transactions");
109 }
110
111 System.out.println();
112
113 System.out.println("=== Malformed request — IllegalArgumentException ===");
114 try {
115 validator.processTransaction(
116 new TransactionRequest(null, 100.0, "M-Flipkart", "DEBIT"),
117 true, 0.0);
118 } catch (IllegalArgumentException iae) {
119 System.out.println("Caught: " + iae.getMessage());
120 } catch (SpendingLimitExceededException slee) {
121 System.out.println("Limit exceeded: " + slee.getMessage());
122 }
123 }
124}Output:
=== Valid transaction ===
APPROVED: W-1001 debited Rs.5000.00 to M-Swiggy
=== Inactive wallet — IllegalStateException ===
Caught: Wallet W-1002 is not active — cannot process transaction
=== Fraud blocklist — SecurityException ===
Caught: Merchant BLOCKED-MERCHANT is on the fraud blocklist
=== Daily limit exceeded — checked exception ===
Limit exceeded: requested Rs.30000.00, remaining Rs.20000.00
Action: suggest splitting into multiple transactions
=== Malformed request — IllegalArgumentException ===
Caught: walletId is required
Best Practices
Throw the most specific exception type that exists for the failure. IllegalArgumentException for bad arguments, IllegalStateException for wrong object state, NullPointerException for explicit fail-fast null checks, and a custom exception when the JDK has no type that captures the domain meaning. A generic throw new RuntimeException("error") tells callers nothing about what kind of error occurred or how to respond to it.
Always include the actual failing values in the exception message. "Discount must be 0-100, got: 150" is debuggable from a log line alone. "Invalid discount" requires reproducing the failure to find out what value caused it. During code reviews, a throw with a message that does not include the offending value is worth flagging — the cost of adding it is near zero and the debugging value is significant.
When wrapping, always pass the original exception as the cause. throw new ServiceException("context", originalException) — never throw new ServiceException(originalException.getMessage()). The second form creates a new exception with no cause, and getCause() returns null — the original type and stack trace are gone from the chain permanently.
Throw early, throw at the boundary. Validate inputs at the start of a method and throw immediately if they are invalid — do not let invalid data propagate deeper into the call stack where the eventual failure (often a cryptic NullPointerException three calls later) is harder to trace back to its actual cause. "Fail fast" is not just a slogan — it is the difference between a throw with a clear message at the entry point and a confusing stack trace from somewhere unrelated.
Common Mistakes
Mistake 1 — Throwing a Generic Exception With No Context
1// WRONG — tells the caller nothing about what went wrong or why
2static void processOrder(Order order) {
3 if (order.items().isEmpty()) {
4 throw new RuntimeException("Error"); // useless message, generic type
5 }
6}
7
8// CORRECT — specific type, specific message with the actual data
9static void processOrder(Order order) {
10 if (order.items().isEmpty()) {
11 throw new IllegalArgumentException(
12 "Order " + order.orderId() + " has no items — cannot process");
13 }
14}Mistake 2 — Throwing Inside a finally Block Without Care
1// WRONG — exception thrown in finally SUPPRESSES any exception from try
2static void riskyOperation() {
3 try {
4 throw new RuntimeException("Original failure in try");
5 } finally {
6 throw new RuntimeException("Cleanup failure"); // ORIGINAL is lost
7 }
8}
9// Caller only sees "Cleanup failure" — "Original failure" vanishes
10
11// CORRECT — catch cleanup failures separately, do not let them mask the original
12static void safeOperation() {
13 try {
14 throw new RuntimeException("Original failure in try");
15 } finally {
16 try {
17 cleanup(); // may throw
18 } catch (Exception cleanupException) {
19 System.err.println("Cleanup failed: " + cleanupException.getMessage());
20 // Logged, not thrown — does not mask the original exception
21 }
22 }
23}
24
25static void cleanup() { /* may throw */ }Mistake 3 — Throwing a Checked Exception Without Declaring throws
1// WRONG — compile error: unreported exception IOException must be caught or declared
2static String readConfig(String path) {
3 if (path == null) {
4 throw new java.io.IOException("Path is null"); // COMPILE ERROR
5 }
6 return "config";
7}
8
9// CORRECT — either declare throws...
10static String readConfig(String path) throws java.io.IOException {
11 if (path == null) {
12 throw new java.io.IOException("Path is null"); // now valid
13 }
14 return "config";
15}
16
17// ...OR use an unchecked exception if the failure is a programming error
18static String readConfigUnchecked(String path) {
19 if (path == null) {
20 throw new IllegalArgumentException("Path is null"); // unchecked — no throws needed
21 }
22 return "config";
23}Mistake 4 — Reassigning the Exception Variable Before Rethrowing (Losing Information)
1// WRONG — creates a new exception, discarding the original's stack trace and type
2static void process() throws Exception {
3 try {
4 riskyOperation();
5 } catch (java.sql.SQLException sqlException) {
6 Exception newException = new Exception(sqlException.getMessage()); // message ONLY
7 throw newException; // original SQLException type and stack trace are GONE
8 }
9}
10
11// CORRECT — rethrow the original directly, or wrap with cause preserved
12static void processCorrect() throws Exception {
13 try {
14 riskyOperation();
15 } catch (java.sql.SQLException sqlException) {
16 throw sqlException; // SAME object — full type and stack trace preserved
17 // OR: throw new ServiceException("context", sqlException); // wrapped with cause
18 }
19}
20
21static void riskyOperation() throws java.sql.SQLException {
22 throw new java.sql.SQLException("DB error");
23}Interview Questions
Q1. What does the throw keyword do in Java?
throw is a statement that raises an exception object at the exact point it appears in code. Its operand must be an instance of Throwable or a subclass. When throw executes, the JVM immediately stops normal execution of the current method, and begins searching for a matching catch block — first in the current method's exception table, then by unwinding the call stack to the caller, continuing until a handler is found or the thread terminates. The stack trace attached to the exception object is captured at the moment of construction (when new SomeException(...) runs), not when throw executes or when the exception is eventually caught.
Q2. What is the difference between throw and throws?
throw is a statement executed at runtime that raises one specific exception object — throw new IllegalArgumentException("bad input"). throws is a declaration in a method signature that lists checked exceptions the method may propagate to its callers — public void readFile() throws IOException. A method can declare throws IOException without ever directly executing a throw statement for IOException — if it calls another method that throws it and does not catch it, the declaration is still required. Conversely, a method can execute throw for an unchecked exception without any throws declaration at all.
Q3. Can you throw a checked exception without declaring it in the method signature?
No — this is a compile error. If a method contains throw new IOException("message") (or calls another method that can throw IOException without catching it), the method must declare throws IOException in its signature, or the code does not compile with the message "unreported exception... must be caught or declared to be thrown." This requirement applies only to checked exceptions — RuntimeException subclasses and Error subclasses can be thrown freely without any throws declaration.
Q4. What happens when you throw inside a catch block — does it always create a new exception?
No. throw exceptionVariable inside a catch block rethrows the same exception object that was caught — same type, same message, same original stack trace. This is different from throw new SomeException(exceptionVariable.getMessage()), which constructs a brand new exception object with a fresh stack trace captured at the new throw site, and loses the original type unless explicitly passed as the cause argument. Both patterns are valid depending on intent: rethrow the same object to propagate unchanged; construct a new wrapping exception to add domain context while preserving the original via getCause().
Q5. Where is the stack trace captured when you throw an exception?
The stack trace is captured during the construction of the exception object — inside the Throwable constructor, which calls fillInStackTrace(). This happens at new SomeException(...), before throw even executes. If you construct an exception object, store it in a variable, and throw it later from a different location, the stack trace reflects where it was constructed, not where it was thrown. This is a subtle point that occasionally appears in product-company interviews to test whether candidates understand the construction-vs-throw distinction.
Q6. Why should you pass the original exception as the cause when throwing a new wrapping exception?
Throwable has a cause field, set via the constructor new SomeException(message, cause) or via initCause(cause). When you catch an exception and throw a different one to add context, passing the original as cause preserves the entire diagnostic chain — getCause() returns the original, printStackTrace() prints both the new exception and the original's stack trace under a "Caused by:" section. If you omit the cause — throw new ServiceException(original.getMessage()) — the new exception's getCause() returns null, and in production logs the original exception type and its stack trace are permanently lost. This single practice is one of the most consistently checked items in code reviews at companies with mature exception handling standards.
FAQs
Can you throw null in Java?
throw null; compiles, but at runtime it immediately throws NullPointerException — because the JVM cannot dispatch a null reference as an exception, so it substitutes an NPE. This is rarely intentional and is a code smell if it appears: it usually means an exception variable was expected to be non-null but was not initialized.
Can a constructor throw an exception in Java?
Yes. Constructors can contain throw statements just like any method, and they can declare throws for checked exceptions. A common pattern: validate constructor arguments and throw new IllegalArgumentException(...) if they violate invariants — this prevents an invalid object from ever being constructed. If a constructor throws, the object is not created — no partially-constructed instance exists for the caller to reference.
Can you throw an exception from a static initializer block?
Yes, but with a caveat: if a static initializer block throws an exception (and it is not a subclass of Error), the JVM wraps it in ExceptionInInitializerError (which IS an Error). This happens because static initializers run during class loading, and class loading failures are treated as severe — ExceptionInInitializerError signals that the class could not be properly initialized, which typically makes the class unusable for the remainder of the JVM's lifetime.
Does throw work inside a lambda expression?
Yes for unchecked exceptions — list.forEach(item -> { if (item == null) throw new IllegalArgumentException("null item"); }); compiles and works normally. For checked exceptions, it is more restrictive: standard functional interfaces like Consumer<T> and Function<T,R> do not declare throws on their abstract methods, so a lambda implementing them cannot directly throw a checked exception without wrapping it in an unchecked exception or using a custom functional interface that declares the checked exception.
What is the difference between throwing an exception and returning an error code?
Returning an error code (like a negative integer or a special enum value) requires every caller to remember to check the return value — if they forget, the program continues with invalid data and the failure surfaces later, far from its origin. throw makes the failure impossible to silently ignore: execution stops immediately, and the exception must be explicitly caught or it terminates the thread. The tradeoff is performance — exception construction is more expensive than returning a value — which is why exceptions are reserved for exceptional conditions, not routine outcomes.
Can throw be used to exit a loop early?
Technically yes — throwing inside a loop and catching outside it does exit the loop. But this is poor practice for normal control flow: break, return, and continue exist precisely for this purpose and are far cheaper (no exception object construction, no stack trace capture). throw should be reserved for situations that are genuinely exceptional — not as a substitute for break when a search finds what it is looking for.
Summary
throw is the statement that turns "something went wrong" into a concrete, typed object carrying a message, an optional cause, and a captured stack trace — and immediately hands control to whatever code is positioned to handle it. The operand must be Throwable or a subclass; for checked exception types, the enclosing method must declare throws.
The distinction between throw (a runtime statement raising one object) and throws (a compile-time declaration listing possible checked exceptions) trips up almost every beginner at least once — and remains a reliable interview screening question precisely because it reveals whether a candidate understands Java's exception model or has only memorised the syntax.
Three practices separate production-quality throw statements from textbook ones: choosing the most specific exception type available, including the actual failing values in the message, and — when wrapping — always passing the original exception as the cause. Each of these costs almost nothing to do correctly and saves significant debugging time when the exception eventually appears in a production log.
What to Read Next
| Topic | Link |
|---|---|
| How try, catch, and multi-catch work — the full handler-side reference | Java try-catch → |
| What finally guarantees and how it interacts with throw and return | Java finally → |
| The full difference between checked and unchecked exceptions and design guidance | Java Checked vs Unchecked Exceptions → |
| How to design custom exception classes with the right fields and hierarchy position | Java Custom Exceptions → |
| The complete exception handling foundation — all five mechanisms together | Java Exception Handling → |