Java Tutorial
🔍

Java try-catch

Java try-catch

The try-catch block is the core syntax of Java's exception handling mechanism. Code that might throw goes inside the try block. If it throws, execution jumps to the matching catch block where the failure is handled. Normal execution continues after the catch block, as if the exception never left the try. Without this structure, any runtime failure immediately crashes the thread with a stack trace and no recovery path.

What Is try-catch?

try-catch is a two-part control structure. The try block marks code that may throw an exception. One or more catch blocks follow it, each declaring the type it handles. When an exception is thrown inside try, the JVM stops executing the try body and searches the catch blocks from top to bottom — the first one whose declared type matches the thrown exception executes.

EXECUTION FLOW:

  try                                 try
    statement 1                         statement 1
    statement 2 ← throws               statement 2
    statement 3   NOT reached           statement 3 ← all run
  catch (SomeException e)                               if no throw
    handle the failure              (no catch executes)

  Execution continues here          Execution continues here
  (after the catch block)           (after the try-catch)

Basic Overview — All try-catch Forms

FORM 1 — Basic try-catch:
  try {
      riskyOperation();
  } catch (SomeException exception) {
      handleFailure(exception);
  }

FORM 2 — Multiple catch blocks (specific before general):
  try {
      operation();
  } catch (FileNotFoundException fnfe) {   ← more specific first
      handleMissing(fnfe);
  } catch (IOException ioe) {               ← general after
      handleIO(ioe);
  } catch (Exception e) {                   ← most general last
      handleUnexpected(e);
  }

FORM 3 — Multi-catch (Java 7+, unrelated exception types):
  try {
      operation();
  } catch (IOException | SQLException combinedException) {
      handleInfraFailure(combinedException);
  }

FORM 4 — try-catch-finally:
  try {
      riskyOperation();
  } catch (SomeException exception) {
      handleFailure(exception);
  } finally {
      cleanup(); // always runs — exception or not
  }

FORM 5 — Nested try-catch:
  try {
      outer operation...
      try {
          inner operation...
      } catch (SpecificException inner) {
          handleInner(inner);
      }
  } catch (GeneralException outer) {
      handleOuter(outer);
  }

FORM 6 — try-with-resources (Java 7+, AutoCloseable resources):
  try (Connection conn = getConnection();
       Statement stmt = conn.createStatement()) {
      return stmt.executeQuery(sql);
  } catch (SQLException sqlException) {
      handleDbFailure(sqlException);
  }
  // conn and stmt closed automatically — even if an exception is thrown

CATCH BLOCK ORDER RULE:
  More specific exception type MUST appear before more general.
  FileNotFoundException before IOException (FileNotFoundException IS-A IOException).
  IOException before Exception.
  Compiler rejects: catch(IOException) followed by catch(FileNotFoundException).

MULTI-CATCH RULES:
  Exceptions in a multi-catch must not be in a subtype relationship with each other.
  catch (IOException | FileNotFoundException) ← COMPILE ERROR: FileNotFoundException IS-A IOException
  catch (IOException | SQLException)          ← VALID: no subtype relationship
  Variable in multi-catch is implicitly final — cannot be reassigned.

How try-catch Works Internally

The JVM implements exception handling using an exception table compiled into each method's bytecode. The table maps instruction ranges in the try block to handler addresses in the catch blocks and the exception types they handle.

EXCEPTION TABLE (simplified bytecode view):

  Method: processPayment()
  ┌─────────────┬─────────────┬──────────────────────────────┐
  │  from (PC)  │  to (PC)    │  handler (PC)  │  exception  │
  ├─────────────┼─────────────┼────────────────┼─────────────┤
  │     0       │    24       │       30       │  IOException│
  │     0       │    24       │       45       │  Exception  │
  └─────────────┴─────────────┴────────────────┴─────────────┘

  PC = program counter (bytecode instruction offset)

EXCEPTION MATCHING PROCESS:
  1. Exception thrown at instruction PC=15 (inside try block)
  2. JVM scans exception table from first to last entry
  3. Checks: from <= 15 <= to? YES for both rows
  4. Checks: is thrown exception instanceof IOException? YES → jump to handler at PC=30
  5. IOException catch block executes
  6. If IOException check had failed → check Exception (second row) → YES → handler at PC=45

STACK UNWINDING — when no catch block matches in current method:
  1. Current stack frame is removed
  2. Exception is re-raised in the calling method
  3. That method's exception table is consulted
  4. Continues until a match is found or the thread terminates

Syntax and Usage

Basic try-catch

The simplest form: one try block, one catch block. Execution of the try body stops at the point of the exception — statements after the throw are not executed. The catch block runs, then execution continues with the code after the entire try-catch structure.

1// File: BasicTryCatchDemo.java 2 3public class BasicTryCatchDemo { 4 5 static double divide(int numerator, int denominator) { 6 return (double) numerator / denominator; // ArithmeticException if denominator is 0 int 7 } 8 9 static int parseAndDouble(String input) { 10 int value = Integer.parseInt(input); // NumberFormatException if input is not numeric 11 System.out.println("Parsed: " + value); // only runs if parseInt succeeds 12 return value * 2; 13 } 14 15 public static void main(String[] args) { 16 17 System.out.println("=== try-catch basic flow ==="); 18 try { 19 System.out.println("Before parse"); 20 int result = parseAndDouble("not-a-number"); // throws here 21 System.out.println("After parse: " + result); // never reached 22 } catch (NumberFormatException nfe) { 23 // Execution jumps here immediately when NumberFormatException is thrown 24 System.out.println("Caught: " + nfe.getMessage()); 25 } 26 // Execution resumes here — program continues normally 27 System.out.println("Execution continues after try-catch"); 28 29 System.out.println(); 30 31 System.out.println("=== No exception — catch block is skipped ==="); 32 try { 33 System.out.println("Before parse"); 34 int result = parseAndDouble("42"); // succeeds — no exception 35 System.out.println("After parse: " + result); 36 } catch (NumberFormatException nfe) { 37 System.out.println("This never runs when no exception is thrown"); 38 } 39 System.out.println("Execution continues after try-catch"); 40 41 System.out.println(); 42 43 System.out.println("=== getMessage(), getCause(), getClass() on caught exception ==="); 44 try { 45 Integer.parseInt("abc"); 46 } catch (NumberFormatException nfe) { 47 System.out.println("Type : " + nfe.getClass().getSimpleName()); 48 System.out.println("Message : " + nfe.getMessage()); 49 System.out.println("Cause : " + nfe.getCause()); // null — no wrapping cause 50 } 51 } 52}
Output:
=== try-catch basic flow ===
Before parse
Caught: For input string: "not-a-number"
Execution continues after try-catch

=== No exception — catch block is skipped ===
Before parse
Parsed: 42
After parse: 84
Execution continues after try-catch

=== getMessage(), getCause(), getClass() on caught exception ===
Type    : NumberFormatException
Message : For input string: "abc"
Cause   : null

Multiple Catch Blocks and Catch Ordering

Multiple catch blocks handle different exception types from the same try block. The JVM evaluates them top to bottom — the first match wins. This ordering is why specific exception types must appear before general ones.

1// File: MultipleCatchDemo.java 2 3import java.io.FileNotFoundException; 4import java.io.IOException; 5 6public class MultipleCatchDemo { 7 8 static String readData(String path, String key) throws IOException { 9 if (path == null) { 10 throw new IllegalArgumentException("Path must not be null"); 11 } 12 if (path.endsWith(".missing")) { 13 throw new FileNotFoundException("File not found: " + path); 14 } 15 if (path.endsWith(".corrupt")) { 16 throw new IOException("File corrupt or unreadable: " + path); 17 } 18 return "value-for-" + key + "-from-" + path; 19 } 20 21 static void loadConfig(String path, String key) { 22 System.out.println("Loading key=" + key + " from path=" + path); 23 try { 24 String value = readData(path, key); 25 System.out.println(" Result: " + value); 26 27 } catch (IllegalArgumentException iae) { 28 // Unchecked — catches before IOException so bad arguments are clear 29 System.out.println(" Bad argument: " + iae.getMessage()); 30 31 } catch (FileNotFoundException fnfe) { 32 // More specific than IOException — must appear BEFORE catch(IOException) 33 // Compiler rejects if IOException comes first 34 System.out.println(" File missing [" + fnfe.getMessage() + "] — using defaults"); 35 36 } catch (IOException ioe) { 37 // General I/O handler — catches remaining IOException subclasses 38 System.out.println(" I/O failure: " + ioe.getMessage()); 39 } 40 System.out.println(" Continued after catch"); 41 } 42 43 public static void main(String[] args) { 44 loadConfig("app.properties", "db.host"); 45 System.out.println(); 46 loadConfig("app.missing", "db.host"); 47 System.out.println(); 48 loadConfig("app.corrupt", "db.port"); 49 System.out.println(); 50 loadConfig(null, "db.name"); 51 } 52}
Output:
Loading key=db.host from path=app.properties
  Result: value-for-db.host-from-app.properties
  Continued after catch

Loading key=db.host from path=app.missing
  File missing [File not found: app.missing] — using defaults
  Continued after catch

Loading key=db.port from path=app.corrupt
  I/O failure: File corrupt or unreadable: app.corrupt
  Continued after catch

Loading key=db.name from path=null
  Bad argument: Path must not be null
  Continued after catch

Multi-Catch — Java 7+

When two unrelated exception types require identical handling, multi-catch eliminates duplicate blocks. The exception variable in a multi-catch is implicitly final — you cannot reassign it inside the block.

1// File: MultiCatchDemo.java 2 3import java.io.IOException; 4import java.sql.SQLException; 5 6public class MultiCatchDemo { 7 8 static void processRecord(String source, String id) 9 throws IOException, SQLException { 10 if (source.equals("file") && id.equals("bad")) { 11 throw new IOException("Failed to read record " + id + " from file"); 12 } 13 if (source.equals("db") && id.equals("bad")) { 14 throw new SQLException("DB error reading record " + id); 15 } 16 System.out.println("Processed: " + id + " from " + source); 17 } 18 19 // BEFORE multi-catch (Java 6): duplicated catch blocks 20 static void withoutMultiCatch(String source, String id) { 21 try { 22 processRecord(source, id); 23 } catch (IOException ioe) { 24 System.out.println("Infra failure [IOException]: " + ioe.getMessage()); 25 } catch (SQLException sqle) { 26 System.out.println("Infra failure [SQLException]: " + sqle.getMessage()); 27 } 28 // Both blocks are identical — violation of DRY 29 } 30 31 // AFTER multi-catch (Java 7+): single handler for both 32 static void withMultiCatch(String source, String id) { 33 try { 34 processRecord(source, id); 35 } catch (IOException | SQLException combinedException) { 36 // combinedException is implicitly final — cannot reassign 37 // getClass().getSimpleName() gives the actual thrown type for logging 38 System.out.println("Infra failure [" + 39 combinedException.getClass().getSimpleName() + "]: " + 40 combinedException.getMessage()); 41 } 42 } 43 44 public static void main(String[] args) { 45 46 System.out.println("=== withMultiCatch — clean handling ==="); 47 withMultiCatch("file", "record-001"); // success 48 withMultiCatch("file", "bad"); // IOException 49 withMultiCatch("db", "bad"); // SQLException 50 51 System.out.println(); 52 53 // Compiler rejects subtypes in same multi-catch: 54 // catch (IOException | FileNotFoundException e) ← COMPILE ERROR 55 // FileNotFoundException IS-A IOException — redundant type in multi-catch 56 System.out.println("Multi-catch with subtype relationship → compile error"); 57 System.out.println("catch (IOException | FileNotFoundException) is INVALID"); 58 System.out.println("Use catch (IOException) alone — it already catches FNFE"); 59 } 60}
Output:
=== withMultiCatch — clean handling ===
Processed: record-001 from file
Infra failure [IOException]: Failed to read record bad from file
Infra failure [SQLException]: DB error reading record bad

Multi-catch with subtype relationship → compile error
catch (IOException | FileNotFoundException) is INVALID
Use catch (IOException) alone — it already catches FNFE

Nested try-catch and Rethrowing

Nesting try-catch blocks handles cases where inner operations need independent error handling separate from the outer context. Rethrowing rethrows the caught exception — either as-is or wrapped — when the current layer cannot handle it meaningfully.

1// File: NestedRethrowDemo.java 2 3import java.io.IOException; 4 5public class NestedRethrowDemo { 6 7 static class ServiceException extends RuntimeException { 8 ServiceException(String message, Throwable cause) { 9 super(message, cause); // always pass cause — preserves the root chain 10 } 11 } 12 13 // Nested try-catch: outer handles DB failure, inner handles file failure separately 14 static void processWithFallback(String primarySource, String fallbackSource) { 15 System.out.println("Attempting primary: " + primarySource); 16 try { 17 readSource(primarySource); // may throw IOException 18 System.out.println("Primary succeeded"); 19 20 } catch (IOException primaryException) { 21 System.out.println("Primary failed: " + primaryException.getMessage() + 22 " — trying fallback: " + fallbackSource); 23 try { 24 readSource(fallbackSource); // inner try-catch for fallback attempt 25 System.out.println("Fallback succeeded"); 26 27 } catch (IOException fallbackException) { 28 // Both primary and fallback failed — give up with context from both 29 System.out.println("Both sources failed — primary: [" + 30 primaryException.getMessage() + "] fallback: [" + 31 fallbackException.getMessage() + "]"); 32 } 33 } 34 } 35 36 // Rethrowing — wrapping a checked exception as unchecked with domain context 37 static String fetchConfig(String key) { 38 try { 39 return readSource("config/" + key); 40 } catch (IOException ioException) { 41 // Cannot handle IOException here — translate to domain exception 42 // ALWAYS pass the original as cause — never lose the root cause 43 throw new ServiceException( 44 "Config unavailable for key: " + key, ioException); 45 } 46 } 47 48 // Rethrowing the same exception after logging 49 static void processWithLogging(String source) throws IOException { 50 try { 51 readSource(source); 52 } catch (IOException ioException) { 53 System.out.println("LOG: Failed to read " + source + 54 " — " + ioException.getMessage()); 55 throw ioException; // rethrow the same exception unchanged 56 } 57 } 58 59 static String readSource(String source) throws IOException { 60 if (source.contains("FAIL") || source.contains("missing")) { 61 throw new IOException("Cannot read: " + source); 62 } 63 return "content-from-" + source; 64 } 65 66 public static void main(String[] args) { 67 68 System.out.println("=== Nested try-catch with fallback ==="); 69 processWithFallback("primary.missing", "fallback.good"); 70 System.out.println(); 71 processWithFallback("primary.missing", "fallback.missing"); 72 73 System.out.println(); 74 75 System.out.println("=== Rethrowing as domain exception ==="); 76 try { 77 fetchConfig("db.host.missing"); // triggers rethrow 78 } catch (ServiceException se) { 79 System.out.println("Caught ServiceException: " + se.getMessage()); 80 System.out.println("Root cause: " + se.getCause().getMessage()); 81 } 82 83 System.out.println(); 84 85 System.out.println("=== Rethrow same exception after logging ==="); 86 try { 87 processWithLogging("data.FAIL"); 88 } catch (IOException ioe) { 89 System.out.println("Caller caught the rethrown: " + ioe.getMessage()); 90 } 91 } 92}
Output:
=== Nested try-catch with fallback ===
Attempting primary: primary.missing
Primary failed: Cannot read: primary.missing — trying fallback: fallback.good
Fallback succeeded

Attempting primary: primary.missing
Primary failed: Cannot read: primary.missing — trying fallback: fallback.missing
Both sources failed — primary: [Cannot read: primary.missing] fallback: [Cannot read: fallback.missing]

=== Rethrowing as domain exception ===
Caught ServiceException: Config unavailable for key: db.host.missing
Root cause: Cannot read: config/db.host.missing

=== Rethrow same exception after logging ===
LOG: Failed to read data.FAIL — Cannot read: data.FAIL
Caller caught the rethrown: Cannot read: data.FAIL

Real-World Example — Meesho Product Import Service

A product import service at Meesho reads product data from seller-uploaded files, validates each record, and imports valid products into the catalog. Different failures require different handling: file access errors abort the entire import; per-row validation errors skip that row but continue; unexpected errors are logged for investigation without crashing the batch.

1// File: ImportResult.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class ImportResult { 7 8 private int succeeded = 0; 9 private int failed = 0; 10 private final List<String> failedRows = new ArrayList<>(); 11 12 public void recordSuccess() { succeeded++; } 13 public void recordFailure(int row, String reason) { 14 failed++; 15 failedRows.add("Row " + row + ": " + reason); 16 } 17 18 public int getSucceeded() { return succeeded; } 19 public int getFailed() { return failed; } 20 public List<String> getFailedRows() { return List.copyOf(failedRows); } 21 22 @Override 23 public String toString() { 24 return String.format("ImportResult{succeeded=%d, failed=%d}", succeeded, failed); 25 } 26}
1// File: ProductRecord.java 2 3public record ProductRecord(int rowNumber, String sku, String name, double price, int stock) {}
1// File: ProductImportService.java 2 3import java.io.IOException; 4import java.util.List; 5 6public class ProductImportService { 7 8 // ---- Simulated external dependencies ---- 9 10 static List<ProductRecord> readFile(String filePath) throws IOException { 11 if (filePath.contains("missing")) { 12 throw new IOException("File not found: " + filePath); 13 } 14 if (filePath.contains("corrupt")) { 15 throw new IOException("File is corrupt or unreadable: " + filePath); 16 } 17 // Simulated file content — row 3 has invalid price, row 5 has blank SKU 18 return List.of( 19 new ProductRecord(1, "SKU-001", "Cotton Kurti", 899.0, 50), 20 new ProductRecord(2, "SKU-002", "Jeans", 1299.0, 30), 21 new ProductRecord(3, "SKU-003", "Saree", -50.0, 20), // invalid price 22 new ProductRecord(4, "SKU-004", "Ethnic Jacket", 1799.0, 15), 23 new ProductRecord(5, "", "Leggings", 499.0, 40), // blank SKU 24 new ProductRecord(6, "SKU-006", "Dupatta", 299.0, 60) 25 ); 26 } 27 28 static void saveToCatalog(ProductRecord record) { 29 if (record.sku().equals("SKU-004")) { 30 // Simulates an unexpected DB save failure for row 4 31 throw new RuntimeException("Catalog DB write failed for: " + record.sku()); 32 } 33 System.out.printf(" Saved: [%s] %-20s Rs.%.0f stock=%d%n", 34 record.sku(), record.name(), record.price(), record.stock()); 35 } 36 37 // ---- Validation ---- 38 39 static void validateRecord(ProductRecord record) { 40 if (record.sku() == null || record.sku().isBlank()) { 41 throw new IllegalArgumentException("SKU is required"); 42 } 43 if (record.price() <= 0) { 44 throw new IllegalArgumentException( 45 "Price must be positive: " + record.price()); 46 } 47 if (record.stock() < 0) { 48 throw new IllegalArgumentException( 49 "Stock cannot be negative: " + record.stock()); 50 } 51 } 52 53 // ---- Main import method ---- 54 55 public ImportResult importProducts(String filePath) { 56 ImportResult result = new ImportResult(); 57 List<ProductRecord> records; 58 59 System.out.println("Starting import from: " + filePath); 60 61 // File-level failure: IOException aborts the entire import 62 try { 63 records = readFile(filePath); 64 } catch (IOException ioException) { 65 System.out.println(" ABORTED: " + ioException.getMessage()); 66 return result; // return empty result — nothing was imported 67 } 68 69 System.out.println(" Read " + records.size() + " records. Processing..."); 70 71 for (ProductRecord record : records) { 72 try { 73 // Validation failure: skip this row, continue the loop 74 validateRecord(record); 75 76 // Unexpected failure: log, skip, continue — do not crash the batch 77 saveToCatalog(record); 78 result.recordSuccess(); 79 80 } catch (IllegalArgumentException validationException) { 81 // Expected: row data does not meet business rules 82 result.recordFailure(record.rowNumber(), 83 "Validation: " + validationException.getMessage()); 84 System.out.printf(" Row %d skipped: %s%n", 85 record.rowNumber(), validationException.getMessage()); 86 87 } catch (Exception unexpectedException) { 88 // Unexpected: log full detail for investigation, continue batch 89 result.recordFailure(record.rowNumber(), 90 "Unexpected: " + unexpectedException.getMessage()); 91 System.out.printf(" Row %d unexpected error [%s]: %s%n", 92 record.rowNumber(), 93 unexpectedException.getClass().getSimpleName(), 94 unexpectedException.getMessage()); 95 } 96 } 97 98 System.out.println("Import complete: " + result); 99 if (!result.getFailedRows().isEmpty()) { 100 System.out.println("Failed rows:"); 101 result.getFailedRows().forEach(r -> System.out.println(" " + r)); 102 } 103 return result; 104 } 105 106 public static void main(String[] args) { 107 ProductImportService service = new ProductImportService(); 108 109 System.out.println("=".repeat(52)); 110 service.importProducts("seller-catalog.csv"); 111 112 System.out.println(); 113 System.out.println("=".repeat(52)); 114 service.importProducts("seller-catalog.missing.csv"); 115 } 116}
Output:
====================================================
Starting import from: seller-catalog.csv
  Read 6 records. Processing...
  Saved: [SKU-001] Cotton Kurti         Rs.899  stock=50
  Saved: [SKU-002] Jeans                Rs.1299 stock=30
  Row 3 skipped: Price must be positive: -50.0
  Row 4 unexpected error [RuntimeException]: Catalog DB write failed for: SKU-004
  Row 5 skipped: SKU is required
  Saved: [SKU-006] Dupatta              Rs.299  stock=60
Import complete: ImportResult{succeeded=3, failed=3}
Failed rows:
  Row 3: Validation: Price must be positive: -50.0
  Row 4: Unexpected: Catalog DB write failed for: SKU-004
  Row 5: Validation: SKU is required

====================================================
Starting import from: seller-catalog.missing.csv
  ABORTED: File not found: seller-catalog.missing.csv

Performance Considerations

The try-catch structure itself has near-zero performance overhead in the happy path — when no exception is thrown. Modern JVMs implement exception handling using the exception table approach: there is no overhead to entering a try block, no runtime check on each statement, and no cost to having try-catch present in frequently-called code.

PERFORMANCE FACTS:

  try block entry and execution (no exception): no overhead
  catch block (never entered):                  no overhead
  Exception object creation (when thrown):      expensive — stack trace captured

  MYTH: "Avoid try-catch in loops because it is slow."
  TRUTH: Empty catch block in a loop costs nothing if no exception is thrown.
         The cost is ONLY at the moment an exception object is constructed.
         Exception construction fills in the full stack trace — that is slow.

  WHAT IS ACTUALLY SLOW:
    Creating exceptions in tight loops when failures are common
    Using try-catch for normal control flow (not-found check)
    Rethrowing an exception and constructing a new one with getCause chain

  WHAT IS FAST:
    try-catch wrapping I/O, DB, and network calls (exceptions are rare)
    Single try-catch at an API boundary catching everything
    try-with-resources for resource cleanup (same as try-finally, not slower)

Best Practices

Catch the most specific exception type first and catch only what you can handle. A catch (IOException e) before catch (FileNotFoundException e) makes the second block unreachable — the compiler rejects it. More broadly, every catch block should contain code that responds meaningfully to that specific failure. A catch (Exception e) block belongs only at the outermost boundary — a REST controller, a thread pool worker, or a batch job's main loop — where the appropriate response is to log and return an error code.

Never write an empty catch block. catch (Exception e) {} is one of the most damaging patterns in a Java codebase. The program continues in an unknown state, no log entry exists, and debugging requires reproducing the failure from scratch. At a minimum: logger.error("Unexpected failure in processPayment", exception). During code reviews, an empty catch block should be treated as a defect.

Always pass the original exception as the cause when wrapping. throw new ServiceException("message", originalException) preserves the root cause chain. Without the cause, getCause() returns null and the original stack trace vanishes from logs. This makes production debugging dramatically harder. The most common missed cause is in the rethrow pattern — throw new RuntimeException(exception.getMessage()) loses the type and stack trace; throw new RuntimeException("context", exception) preserves them.

Use try-with-resources for any resource that implements AutoCloseable. Database connections, prepared statements, file readers, HTTP clients — these all implement AutoCloseable. try (Connection conn = dataSource.getConnection()) automatically calls conn.close() when the block exits, whether normally or via exception. The legacy try-finally pattern for closing resources has two failure modes: forgetting the close call and exception suppression when both try and finally throw. try-with-resources handles both correctly.

Common Mistakes

Mistake 1 — Catching Before Logging and Then Swallowing

1// WRONG — the exception is caught and ignored 2// No record of what went wrong; caller gets a wrong value 3public int parseQuantity(String input) { 4 try { 5 return Integer.parseInt(input); 6 } catch (NumberFormatException nfe) { 7 // Empty — exception swallowed, method silently returns 0 8 } 9 return 0; 10} 11 12// CORRECT — log and return a safe default (or rethrow) 13public int parseQuantity(String input) { 14 try { 15 return Integer.parseInt(input); 16 } catch (NumberFormatException nfe) { 17 System.err.println("Invalid quantity input: [" + input + "] — defaulting to 0"); 18 return 0; 19 } 20}

Mistake 2 — Catching a General Type That Hides a More Serious Failure

1// WRONG — catch(Exception) hides NullPointerException, OutOfMemoryError (via Throwable), 2// and any programming error in the method body 3public String loadUserProfile(String userId) { 4 try { 5 User user = userRepository.findById(userId); // NPE if userRepository is null 6 return user.getProfile().toJson(); // NPE if user or profile is null 7 } catch (Exception e) { 8 return "{}"; // silently returns empty JSON — no indication of what failed 9 } 10} 11 12// CORRECT — handle the expected failure, let unexpected ones propagate 13public String loadUserProfile(String userId) { 14 if (userId == null) throw new IllegalArgumentException("userId required"); 15 User user = userRepository.findById(userId); // NPE propagates if repo is null — it's a bug 16 if (user == null) return "{}"; // expected: user not found 17 if (user.getProfile() == null) return "{}"; // expected: no profile yet 18 return user.getProfile().toJson(); 19}

Mistake 3 — Wrapping Without Preserving the Cause

1// WRONG — original exception's type and stack trace are lost 2public void saveProduct(Product product) { 3 try { 4 database.insert(product); 5 } catch (java.sql.SQLException sqlException) { 6 throw new RuntimeException(sqlException.getMessage()); // type and stack lost 7 } 8} 9 10// CORRECT — pass the original as the second argument 11public void saveProduct(Product product) { 12 try { 13 database.insert(product); 14 } catch (java.sql.SQLException sqlException) { 15 throw new ProductPersistenceException( 16 "Failed to save product: " + product.id(), sqlException); 17 // getCause() returns the SQLException; full chain visible in logs 18 } 19}

Mistake 4 — Placing catch(Exception) Before Specific Types

1// WRONG — compiler rejects: catch(FileNotFoundException) is unreachable 2// after catch(IOException) because FNFE IS-A IOException 3try { 4 openFile(path); 5} catch (IOException ioe) { 6 System.out.println("IO error"); 7} catch (FileNotFoundException fnfe) { // COMPILE ERROR: already caught 8 System.out.println("File missing"); 9} 10 11// CORRECT — specific type first, general type second 12try { 13 openFile(path); 14} catch (FileNotFoundException fnfe) { // specific first 15 System.out.println("File missing: " + fnfe.getMessage()); 16} catch (IOException ioe) { // general second 17 System.out.println("IO error: " + ioe.getMessage()); 18}

Interview Questions

Q1. What is the purpose of the try-catch block in Java?

try-catch is the primary syntax for handling runtime exceptions. Code that may throw is placed inside the try block. When a throw statement executes, the JVM stops executing the try body at that exact point and searches the catch blocks from top to bottom for a handler whose declared type is the same as or a supertype of the thrown exception. The matching catch block executes, then normal execution continues after the entire try-catch structure. Without try-catch, any uncaught exception unwinds the call stack until the thread terminates.

Q2. What happens when no catch block matches the thrown exception?

The JVM removes the current stack frame and re-raises the exception in the calling method, consulting that method's exception table. This unwinding continues through the entire call stack until either a matching catch block is found or the exception reaches the thread's uncaught exception handler. The default uncaught exception handler prints the exception type, message, and full stack trace to standard error and terminates the thread.

Q3. Why must more specific catch blocks appear before more general ones?

Because the JVM evaluates catch blocks in order using instanceof matching — the first block whose declared type is a supertype of the thrown exception executes. If catch (IOException e) appears before catch (FileNotFoundException e), every FileNotFoundException matches the IOException block first (because FileNotFoundException IS-A IOException), and the FileNotFoundException block never executes. The compiler detects this as unreachable code and produces a compile error.

Q4. What is multi-catch syntax and when should you use it?

Multi-catch (Java 7+) allows a single catch block to handle multiple unrelated exception types: catch (IOException | SQLException e). Use it when two or more exception types require identical handling and duplicating the catch block would violate DRY. The variable in a multi-catch is implicitly final — you cannot reassign it. You cannot use multi-catch for exception types in a subtype relationship (IOException | FileNotFoundException is a compile error because FileNotFoundException IS-A IOException).

Q5. What is the difference between rethrowing an exception and wrapping it?

Rethrowing reraises the same exception object: throw exception. The stack trace still originates at the original throw point. Wrapping creates a new exception with the original as its cause: throw new ServiceException("context", originalException). Wrapping adds domain context while preserving the full root cause chain. Both patterns are valid; the key rule for wrapping is to always pass the original as the cause argument — missing the cause argument creates a wrapper with getCause() == null, which destroys the diagnostic chain in logs.

Q6. Does a try-catch block have any performance impact when no exception is thrown?

No measurable impact in the happy path. Modern JVMs implement exception handling through an exception table — a data structure compiled into the class file that maps instruction ranges to handler addresses. Entering a try block requires no runtime check and no overhead per statement. The performance cost only occurs when an exception is actually thrown: constructing the exception object captures the full stack trace, which is expensive. This is why exceptions should represent exceptional conditions — not normal control flow.

FAQs

Can a try block exist without a catch block?

Yes — try without catch is valid when finally is present: try { ... } finally { ... }. The finally block runs for cleanup regardless of whether an exception was thrown, even if no handler is present. The exception still propagates after finally completes — try-finally without catch does not handle the exception, only cleans up after it.

Can the same exception be caught by multiple catch blocks?

No. Only the first matching catch block executes. Once a catch block matches and runs, the remaining catch blocks for the same try are skipped entirely. The JVM does not evaluate remaining blocks after a match is found.

What happens to the variable declared in the catch clause outside the catch block?

The exception variable (e.g., IOException ioe) is scoped to that specific catch block. It is not accessible before the catch block, after the catch block, or in other catch blocks. In a multi-catch, the single variable is shared by all the listed types but is only accessible within that one block.

Is it valid to have a try block with no statements inside?

Syntactically yes — an empty try block compiles. Practically it serves no purpose and IDE inspection tools flag it as a code smell. An empty try with a non-empty catch means the catch block can never execute (no exception possible from empty code), and the compiler may warn about unreachable catch clauses depending on the exception type.

Does catching an exception clear its stack trace?

No. Catching an exception does not modify the exception object in any way — its message, cause, and stack trace remain intact. printStackTrace() and getStackTrace() return the same data whether the exception was caught zero times or three times. The stack trace is set at construction time and is immutable.

When should you rethrow an exception after catching it?

Rethrow when: (a) you need to log the exception but let it propagate — catch, log, then throw e; (b) you want to wrap it in a domain exception with more context — throw new ServiceException("context", e); or (c) you need to perform cleanup before letting the exception continue — run the cleanup, then throw e or throw new DomainException(e). Do not catch and rethrow without doing anything useful — it adds a try-catch block with no benefit and obscures the code's intent.

Summary

try-catch is the mechanism that separates exception handling from normal execution flow. Code inside try runs normally until a throw; execution then jumps to the first matching catch block, which handles the failure; and normal execution resumes after the entire structure.

Three rules govern correct try-catch usage. Catch blocks are evaluated top-to-bottom, so specific types must appear before general ones — the compiler enforces this. Never leave a catch block empty — the minimum is logging. Always pass the original exception as the cause when wrapping and rethrowing — losing the cause chain is the single most common reason production debugging takes ten times longer than it should.

The try-with-resources form (try (Resource r = ...) { ... }) is the modern replacement for try-finally for AutoCloseable resources. The finally block is the right tool for non-resource cleanup that must always execute. The multi-catch form (catch (IOException | SQLException e)) eliminates duplicate catch blocks for unrelated types.

What to Read Next