iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Exceptions

Java exceptions are objects thrown to signal errors. checked ones (extends Exception) must be declared or caught; unchecked (extends RuntimeException) bubble up freely. Modern Java leans toward unchecked + sealed hierarchies.

Try, catch, throw, try-with-resources

EXAMPLE
// 1) Try / catch
try {
    String body = Files.readString(Path.of("config.json"));
    parse(body);
} catch (IOException e) {
    log.error("failed to read config", e);
    throw new RuntimeException(e);
}

// 2) Multiple catches
try {
    doWork();
} catch (FileNotFoundException e) {
    log.warn("file missing, using defaults");
} catch (IOException e) {
    log.error("IO failed", e);
} catch (Exception e) {
    log.error("unexpected", e);
    throw e;
}

// 3) Multi-catch (Java 7+)
try {
    doWork();
} catch (IOException | SQLException e) {
    log.error("data error", e);
}

// 4) Try-with-resources — auto-close
try (var reader = Files.newBufferedReader(path);
     var writer = Files.newBufferedWriter(out)) {
    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line.toUpperCase() + "\n");
    }
}
// Both closed automatically (in reverse order); IOException possible

// Any object implementing AutoCloseable works:
try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement("SELECT 1")) {
    ResultSet rs = ps.executeQuery();
}

// 5) Throw
throw new IllegalArgumentException("age must be >= 0");
throw new IllegalStateException("transaction not started");
throw new MyDomainException("order not found", orderId);

// 6) Throws declaration (checked exceptions)
public String readConfig() throws IOException {
    return Files.readString(Path.of("config.json"));
}

public void process() throws IOException, SQLException {
    var data = readConfig();
    db.insert(data);
}

// 7) Finally — always runs (unless JVM exits)
try {
    work();
} finally {
    cleanup();        // even if work() throws
}

// Useful for closing resources before try-with-resources existed
// Today: prefer try-with-resources for AutoCloseable

// 8) Custom exceptions
public class OrderNotFoundException extends RuntimeException {
    private final String orderId;
    public OrderNotFoundException(String orderId) {
        super("Order not found: " + orderId);
        this.orderId = orderId;
    }
    public String getOrderId() { return orderId; }
}

throw new OrderNotFoundException("o_42");

// 9) Sealed exception hierarchies (Java 17+)
public sealed class PaymentException extends RuntimeException
    permits CardDeclinedException, GatewayDownException, InvalidAmountException {

    private final String code;
    public PaymentException(String code, String message) {
        super(message);
        this.code = code;
    }
    public String getCode() { return code; }
}

public final class CardDeclinedException extends PaymentException {
    public CardDeclinedException(String message) { super("CARD_DECLINED", message); }
}
public final class GatewayDownException  extends PaymentException {
    public GatewayDownException(String message)  { super("GATEWAY_DOWN", message); }
}
public final class InvalidAmountException extends PaymentException {
    public InvalidAmountException(String message) { super("INVALID_AMOUNT", message); }
}

// Handler — pattern matching on sealed type
String handle(PaymentException e) {
    return switch (e) {
        case CardDeclinedException c -> "Card declined: " + c.getMessage();
        case GatewayDownException g  -> "Try again later";
        case InvalidAmountException a -> "Bad amount";
    };
}

// 10) Re-throwing with context — wrap in a new exception
try {
    api.call();
} catch (IOException e) {
    throw new ServiceException("API call failed", e);   // cause chain preserved
}

// Get the cause later
catch (ServiceException e) {
    Throwable cause = e.getCause();
    log.error("caused by:", cause);
}

// 11) Stack trace
try {
    work();
} catch (Exception e) {
    e.printStackTrace();                              // bad in prod logs
    log.error("work failed", e);                       // structured

    // Manually inspect
    for (var frame : e.getStackTrace()) {
        System.out.println(frame.getClassName() + "." + frame.getMethodName());
    }
}

// Suppress noisy stack traces (e.g. control flow exceptions)
public class SilentException extends RuntimeException {
    public SilentException(String message) {
        super(message, null, false, false);   // disable suppression + stack trace
    }
}

// 12) Checked vs unchecked — when to pick

// Checked (extends Exception):
//   - Caller MUST handle or declare
//   - Compiler enforces
//   - Use for RECOVERABLE conditions (file missing, network blip)
//   - Java standard library uses this style heavily (IOException, SQLException)

// Unchecked (extends RuntimeException):
//   - Caller CAN handle but doesn't have to
//   - For programmer errors (null check failure, illegal arg)
//   - For framework / app errors that callers usually can't recover from
//   - Modern style: use mostly unchecked + Optional for absent values

// 13) Try as expression (Java 14+ enhanced switch is similar, but try isn't an expression yet)
// Workaround: extract to a method
int parsed = parse(input);   // method handles try internally

public int parse(String input) {
    try { return Integer.parseInt(input); }
    catch (NumberFormatException e) { return -1; }
}

// 14) Logging vs throwing
// DON'T:
catch (Exception e) {
    log.error("failed", e);
    throw e;                                          // double-logged
}

// DO ONE OR THE OTHER:
catch (Exception e) {
    throw new RuntimeException("failed", e);          // upstream logger handles it
}

// 15) Resource cleanup patterns

// Bad — finally with exception risk
FileInputStream fis = null;
try {
    fis = new FileInputStream(path);
    // ...
} finally {
    if (fis != null) fis.close();                     // close throws another IOException — confusing
}

// Good — try-with-resources
try (var fis = new FileInputStream(path)) {
    // ...
}

// 16) Performance
//   • Throwing is EXPENSIVE (~5-50μs to capture stack trace)
//   • Don't use exceptions for control flow
//   • Use Optional<T> for absent values, Result<T, E> for fallible (custom)
//   • Stack trace capture dominates throw cost; disable via constructor flag if needed

// 17) Try / catch in streams
List<String> lengths = paths.stream()
    .map(p -> {
        try { return Files.readString(p); }
        catch (IOException e) { return ""; }
    })
    .collect(Collectors.toList());

// Cleaner: helper to wrap checked exceptions
@@FunctionalInterface
interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;
}

static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f) {
    return t -> {
        try { return f.apply(t); }
        catch (Exception e) { throw new RuntimeException(e); }
    };
}

List<String> contents = paths.stream()
    .map(unchecked(Files::readString))
    .toList();

// 18) Test exceptions
// JUnit 5
import static org.junit.jupiter.api.Assertions.*;

assertThrows(OrderNotFoundException.class, () -> service.find("missing"));

var e = assertThrows(PaymentException.class, () -> service.charge(card));
assertEquals("CARD_DECLINED", e.getCode());

// 19) Common bugs
//   • Empty catch blocks — silently swallow errors
//   • Catching Throwable / Exception → catches Error too (OutOfMemoryError, StackOverflowError)
//   • Catching + ignoring InterruptedException → must re-set interrupt flag or re-throw
//   • Using exceptions for control flow (StopIteration-style)
//   • Forgetting to call cause when wrapping — lose stack chain
//   • Catching exception, logging, and re-throwing same — double logging
//   • Wrapping RuntimeException in another RuntimeException — adds noise without value

// 20) InterruptedException — special case
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();           // RE-SET the interrupt flag
    throw new RuntimeException("interrupted", e);
}
// Lose the flag → upstream loops can't tell

// 21) Best practices
//   ✅ Use try-with-resources for any AutoCloseable
//   ✅ Prefer unchecked exceptions for app-level errors
//   ✅ Build a sealed hierarchy for domain errors
//   ✅ Always include cause when wrapping
//   ✅ Throw fast — validate input at the boundary
//   ✅ Don't catch and re-throw at every layer; let exceptions bubble
//   ✅ One catch per type of failure; don't blanket-catch Throwable
//   ✅ Log structured, with context (request_id, user_id)

Why it matters

Try-with-resources is the modern resource cleanup pattern; sealed exception hierarchies + pattern-matching switches give you exhaustive handling. Use unchecked exceptions for most app errors and reserve checked for recoverable I/O conditions.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
try {
    int n = Integer.parseInt(input);
} catch (NumberFormatException e) {
    System.err.println("bad input: " + e.getMessage());
} finally {
    cleanup();
}
Try it Yourself »

Discussion

Loading…