Try Catch

Java exception handling — try-catch-finally, multi-catch, try-with-resources, and the patterns senior developers use to write resilient enterprise code.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: Exception handling lets your program respond gracefully to errors instead of crashing. try contains code that might fail. catch handles specific error types. finally always runs (for cleanup). try-with-resources automatically closes resources. The golden rule: catch exceptions you can handle, let others propagate.

🔍 What Is It?

Exception Hierarchy:

Throwable
├── Error (JVM-level, do not catch)
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception
    ├── RuntimeException (unchecked — no throws declaration needed)
    │   ├── NullPointerException
    │   ├── IllegalArgumentException
    │   ├── IllegalStateException
    │   └── IndexOutOfBoundsException
    └── Checked Exceptions (must declare or handle)
        ├── IOException
        ├── SQLException
        └── ParseException

Checked vs Unchecked:

  • →Checked — compiler forces you to handle or declare with throws. Represents recoverable conditions (file not found, network timeout).
  • →Unchecked (RuntimeException) — compiler does not require handling. Represents programming errors (null pointer, illegal argument).

try-with-resources (Java 7+):

Automatically closes any AutoCloseable resource when the block exits, even if an exception occurs.

🎯 Why Do We Need It?

Exception handling is essential for:

  • →Resilience — handle expected failures (network timeout, DB unavailable) gracefully
  • →Resource cleanup — always close files, connections, streams (try-with-resources)
  • →Error reporting — log the full stack trace for debugging
  • →User experience — show meaningful error messages, not stack traces
  • →Transaction management — rollback transactions on failure

Without exception handling, any unexpected condition crashes the entire application.

🏗️ What Happens in Real Projects?

In enterprise Spring Boot applications:

  • →Service methods catch specific exceptions and translate them to domain exceptions
  • →Controllers use @ExceptionHandler or @ControllerAdvice to translate exceptions to HTTP responses
  • →Repository layer wraps DataAccessException into domain-specific exceptions
  • →try-with-resources is used for every I/O operation
  • →Global exception handlers log and return structured error responses

⚙️ How It Works

Exception Propagation:

  1. Exception is thrown at the point of failure
  2. JVM searches up the call stack for a matching catch block
  3. First matching catch block executes
  4. If no catch found, JVM prints stack trace and terminates the thread

finally block:

Runs after try and catch, regardless of outcome. Used for cleanup that must happen even if an exception occurs. Not needed for resource cleanup if using try-with-resources.

Exception chaining:

When catching one exception and throwing another, always include the original as the cause: throw new ServiceException("message", originalException). This preserves the full diagnostic chain.

💻 Example

Exception Handling in Enterprise Code — Production-quality exception handling patterns.

java
// try-with-resources — automatically closes resources
public String readFile(Path path) throws IOException {
    try (BufferedReader reader = Files.newBufferedReader(path)) {
        return reader.lines().collect(Collectors.joining("
"));
    }
    // reader.close() is called automatically, even if exception occurs
}

// Multiple resources — closed in reverse order
public void copyFile(Path source, Path dest) throws IOException {
    try (InputStream in = Files.newInputStream(source);
         OutputStream out = Files.newOutputStream(dest)) {
        in.transferTo(out);
    }
}

// Multi-catch (Java 7+) — same handler for multiple exception types
public UserDto getUser(String id) {
    try {
        return userRepository.findById(id)
            .map(this::toDto)
            .orElseThrow(() -> new UserNotFoundException(id));
    } catch (IllegalArgumentException | NumberFormatException e) {
        throw new BadRequestException("Invalid user ID format: " + id, e);
    } catch (DataAccessException e) {
        // Always chain the original exception for diagnostics
        throw new ServiceException("Database error fetching user: " + id, e);
    }
}

// finally — cleanup when try-with-resources is not applicable
public void processWithLock(String lockKey) {
    boolean locked = lockService.acquire(lockKey);
    if (!locked) throw new LockAcquisitionException(lockKey);
    
    try {
        doWork();
    } finally {
        lockService.release(lockKey);  // ALWAYS release the lock
    }
}

🤔 Common Confusions

❓ Should I catch Exception or specific exception types?

Always catch the most specific exception type you can handle. Catching Exception (or worse, Throwable) swallows unexpected errors silently. The only legitimate use of catch(Exception e) is in a top-level handler (like @ExceptionHandler) where you log and return an error response. In service/repository code, catch specific exceptions.

❓ What is the difference between checked and unchecked exceptions?

Checked exceptions (extends Exception but not RuntimeException) must be declared in throws or handled with try-catch. They represent conditions the caller can reasonably recover from (file not found, network error). Unchecked exceptions (extends RuntimeException) represent programming errors — invalid arguments, null pointers, illegal state. In modern Java, most frameworks use unchecked exceptions exclusively to avoid the boilerplate of checked exception declarations.

❓ Does finally always run?

Almost always. Exceptions where finally does NOT run: (1) System.exit() is called, (2) JVM crashes, (3) the thread is killed. In normal operation — including when an exception is thrown — finally always runs. This makes it reliable for cleanup like releasing locks, closing connections, and logging.

🚨 Common Production Issues

Swallowing exceptions silently — catch(Exception e) {} with no logging or rethrow makes debugging impossible. Always log or rethrow.

Losing the original exception — throw new ServiceException("msg") without the cause loses the stack trace. Always: throw new ServiceException("msg", originalException).

Catching Error — OutOfMemoryError and StackOverflowError indicate JVM-level problems. Catching them gives a false sense of recovery. Let them propagate.

Exception in finally block — if finally throws an exception, the original exception is lost. Use try-with-resources or suppress exceptions carefully.

✅ Best Practices

Use try-with-resources for all AutoCloseable resources — never rely on finally for resource cleanup.

Always include the original exception as the cause when wrapping: throw new AppException("msg", cause).

Never swallow exceptions silently — always log at minimum.

Catch specific exceptions, not Exception or Throwable.

Use custom exception hierarchy: AppException -> ServiceException, NotFoundException, ValidationException.

Log exceptions at the boundary where they are handled, not at every layer.

🏛️ Architect's Note

Senior Architect's Perspective

In Spring Boot, define a clear exception hierarchy: a base AppException (unchecked), then domain-specific subclasses (UserNotFoundException, InsufficientFundsException, ValidationException). Map these to HTTP status codes in @ControllerAdvice. Service layer throws domain exceptions; controller layer never catches — the advice handles it. This keeps controllers thin and exception handling centralized.

💬 Frequently Asked Questions

What is the difference between throw and throws?

throw (without s) is a statement that throws an exception: throw new IllegalArgumentException("msg"). throws (with s) is part of a method signature declaring that the method may throw a checked exception: public void readFile() throws IOException. You use throw to throw an exception; you use throws to declare that a method might throw a checked exception.

What is exception chaining and why does it matter?

Exception chaining preserves the original cause when wrapping exceptions. If a DataAccessException occurs and you wrap it: throw new ServiceException("DB error", dataAccessException), the original stack trace is preserved as the cause. Without chaining, you lose the root cause and debugging becomes very difficult. Always pass the original exception as the second argument when wrapping.

🎯 Key Takeaways

1try-with-resources automatically closes AutoCloseable resources — always use it for I/O
2Catch specific exceptions, not Exception or Throwable
3Always include the original exception as the cause when wrapping
4finally always runs — use for cleanup that cannot use try-with-resources
5Checked exceptions: compiler-enforced; Unchecked (RuntimeException): programming errors
6In Spring Boot, use @ControllerAdvice for centralized exception-to-HTTP mapping

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide Core Java learning path

Browse the full Core Java topic collection and learning order on SplashRide.

Core Java Interview Prep

Practice Core Java interview questions and scenario-based answers.

Backend Engineer Career Path

A structured journey through Core Java, Spring Boot, production backend engineering, and interview preparation.

Full Stack Java Developer Career Path

A guided route through frontend foundations, Java backend engineering, delivery workflows, and interview readiness.

Throws

The throws keyword, checked vs unchecked exception design, and why modern Java frameworks moved away from checked exceptions.

Custom Exceptions

Building a clean exception hierarchy for enterprise applications — naming conventions, when to extend checked vs unchecked, and how Spring Boot maps them to HTTP responses.