Exception Handling & Multithreading

try/catch/finally, custom exceptions, threads, the Executor framework, and thread-safety basics.

Why exceptions exist

Instead of returning error codes that are easy to ignore, Java forces problems to surface as exceptions — objects that describe what went wrong, thrown up the call stack until something handles them.

Java
public class Main {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            System.out.println("Can't divide by zero: " + e.getMessage());
        } finally {
            System.out.println("This always runs — cleanup goes here");
        }
    }
}
Plaintext
Can't divide by zero: / by zero
This always runs — cleanup goes here

Checked vs unchecked exceptions

Type Examples Must be declared/caught?
Checked IOException, SQLException Yes — the compiler forces you to handle or declare (throws) them
Unchecked (RuntimeException) NullPointerException, IllegalArgumentException No — usually programming bugs, not recoverable conditions
Error OutOfMemoryError, StackOverflowError No — serious JVM-level problems, not meant to be caught

Custom exceptions

Java
public class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

public class BankAccount {
    private double balance;

    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException("Not enough balance");
        }
        balance -= amount;
    }
}

try-with-resources

Resources (files, database connections) that implement AutoCloseable get closed automatically, even if an exception is thrown — no finally block needed:

Java
try (var reader = new BufferedReader(new FileReader("data.txt"))) {
    System.out.println(reader.readLine());
} catch (IOException e) {
    System.out.println("Failed to read file: " + e.getMessage());
}

Threads and concurrency

A thread is an independent path of execution within a program. The JVM can run multiple threads in parallel on multi-core CPUs.

Java
Runnable task = () -> {
    for (int i = 0; i < 3; i++) {
        System.out.println(Thread.currentThread().getName() + ": " + i);
    }
};

Thread t1 = new Thread(task, "Worker-1");
Thread t2 = new Thread(task, "Worker-2");
t1.start();
t2.start();

Because both threads run concurrently, their output can interleave unpredictably — this is exactly why shared state needs protection.

The Executor framework (preferred over raw threads)

Managing raw Thread objects doesn't scale. The ExecutorService manages a pool of reusable worker threads for you:

Java
import java.util.concurrent.*;

ExecutorService pool = Executors.newFixedThreadPool(4);

Future<Integer> future = pool.submit(() -> {
    Thread.sleep(100);
    return 42;
});

System.out.println("Result: " + future.get()); // blocks until the task finishes
pool.shutdown();

Thread safety

When multiple threads mutate shared state without coordination, you get race conditions. synchronized ensures only one thread executes a block at a time:

Java
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int get() {
        return count;
    }
}

For simple counters, java.util.concurrent.atomic.AtomicInteger is faster and lock-free:

Java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

Common mistakes

  • Catching Exception (or worse, Throwable) too broadly, silently swallowing real bugs.
  • Forgetting finally/try-with-resources, leaking file handles or database connections.
  • Mutating shared state from multiple threads without synchronized, an Atomic* type, or a concurrent collection — a classic source of flaky, hard-to-reproduce bugs.
  • Creating a new raw Thread per task in a hot path instead of reusing an ExecutorService pool.

Interview questions

Q: What's the difference between checked and unchecked exceptions? Checked exceptions (subclasses of Exception other than RuntimeException) must be declared with throws or caught — the compiler enforces handling. Unchecked exceptions (RuntimeException and its subclasses) are not enforced by the compiler and usually indicate programming bugs.

Q: Why prefer ExecutorService over creating raw Thread objects? Thread creation is expensive, and unbounded thread creation can exhaust system resources. An ExecutorService reuses a bounded pool of threads, queues excess work, and gives you Future-based results and clean shutdown.

Q: What causes a race condition? Two or more threads reading and writing shared mutable state without synchronization, so the final result depends on unpredictable thread scheduling instead of program logic.