Exceptions, Assertions & Logging
Logging
Logs are the primary diagnostic instrument in production. Use a façade (SLF4J) over a modern backend, log at the right level with parameterized messages, and treat log output as a machine-readable data stream, not console decoration.
- Levels: ERROR (action needed) > WARN (surprising, tolerated) > INFO (lifecycle) > DEBUG > TRACE
- Parameterized logging
log.debug("user {}", id)avoids string-building when disabled - Pass the exception object as the last argument — that preserves the stack trace
- Log an exception once, at the layer that handles it
- In cloud environments: structured (JSON) logs to stdout, correlated by trace IDs (Observability)
- Logging is I/O on the hot path — async appenders for latency-sensitive apps
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
log.info("order {} accepted for {}", orderId, customerId); // parameterized — no concat cost
try {
charge(order);
} catch (PaymentException e) {
log.error("payment failed for order {}", orderId, e); // exception LAST — full trace kept
throw new OrderRejectedException(orderId, e); // …or log OR rethrow, not both
}The JDK ships java.util.logging (Core Java covers it thoroughly), but the ecosystem consolidated on SLF4J as the API with Logback or Log4j2 behind it. The façade matters because libraries must not impose a logging implementation on applications. Configuration (levels per package, appenders, formats) lives outside the code.
The performance books' angle: logging is frequently the hidden bottleneck in high-throughput services — synchronous appenders serialize threads through a lock and an fsync. High-performance setups use async appenders with bounded queues, and confront the trade-off honestly: what happens to log events when the queue fills (block vs drop) is a real design decision (Optimizing Java ch. 14).