Imagine reporting a car accident: you wouldn’t just say “something went wrong.” You’d provide details—location, time, cause. Similarly, in Java, logging exceptions properly is critical for debugging, monitoring, and maintaining applications.
This tutorial explores how to log exceptions effectively with java.util.logging, Log4j, and SLF4J, highlighting best practices, common mistakes, and real-world use cases.
Purpose of Java Exception Handling
Exception handling aims to:
- Catch errors gracefully.
- Communicate failure clearly.
- Provide useful diagnostics for debugging.
- Ensure consistent system behavior under failure.
Real-world analogy: Logging is like a black box in airplanes—it doesn’t prevent crashes but helps explain them.
Errors vs Exceptions
At the root of Java’s throwable hierarchy is Throwable
:
Error
: Irrecoverable problems likeOutOfMemoryError
.Exception
: Recoverable issues likeIOException
.
Exception Hierarchy Overview
Throwable
├── Error (unrecoverable)
│ └── OutOfMemoryError, StackOverflowError
└── Exception
├── Checked (must be declared or handled)
│ └── IOException, SQLException
└── Unchecked (RuntimeException)
└── NullPointerException, ArithmeticException
Why Logging Exceptions Matters
- Debugging: Pinpoints root causes faster.
- Monitoring: Detects anomalies in production.
- Auditing: Provides historical trace of issues.
- Compliance: Some industries require detailed error logs.
Logging with java.util.logging
import java.util.logging.*;
public class Example {
private static final Logger logger = Logger.getLogger(Example.class.getName());
public static void main(String[] args) {
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
logger.log(Level.SEVERE, "Division failed", e);
}
}
}
- Built-in JDK logging.
- Simple, no extra dependencies.
- Limited configurability compared to modern frameworks.
Logging with Log4j
import org.apache.log4j.Logger;
public class ExampleLog4j {
private static final Logger logger = Logger.getLogger(ExampleLog4j.class);
public static void main(String[] args) {
try {
String text = null;
System.out.println(text.length());
} catch (NullPointerException e) {
logger.error("Null pointer occurred", e);
}
}
}
- Flexible and widely used.
- Configurable with XML or properties files.
- Supports rolling logs, async logging.
Logging with SLF4J (with Logback)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExampleSLF4J {
private static final Logger logger = LoggerFactory.getLogger(ExampleSLF4J.class);
public static void main(String[] args) {
try {
int[] arr = new int[2];
arr[5] = 10;
} catch (ArrayIndexOutOfBoundsException e) {
logger.error("Array access error: {}", e.getMessage(), e);
}
}
}
- Standard facade for logging frameworks.
- Encourages decoupling.
- Supports parameterized messages.
Best Practices for Logging Exceptions
- Always log exception with stack trace, not just message.
- Use appropriate log levels (
ERROR
,WARN
,INFO
,DEBUG
). - Avoid duplicate logging (don’t log and rethrow unless necessary).
- Provide context (e.g., user ID, operation name).
- Use structured logging for machine-readable logs.
- Externalize logging config (don’t hardcode).
Common Anti-Patterns
- Swallowing exceptions silently (
catch (Exception e) {}
). - Logging without stack trace (
logger.error(e.getMessage())
). - Over-logging (flooding logs with irrelevant details).
- Using
System.out.println
instead of a logging framework. - Logging sensitive data like passwords.
Real-World Scenarios
File I/O
try (FileReader fr = new FileReader("file.txt")) {
// process
} catch (IOException e) {
logger.error("File processing failed", e);
}
JDBC
try (Connection con = DriverManager.getConnection(url, user, pass)) {
// query
} catch (SQLException e) {
logger.error("DB query failed", e);
}
REST APIs (Spring Boot)
@GetMapping("/users/{id}")
public User getUser(@PathVariable int id) {
try {
return userService.findById(id);
} catch (UserNotFoundException e) {
logger.warn("User not found: {}", id, e);
throw e;
}
}
Multithreading
Future<Integer> f = executor.submit(() -> 10 / 0);
try {
f.get();
} catch (ExecutionException e) {
logger.error("Task failed", e.getCause());
}
📌 What's New in Java Exception Handling
- Java 7+: Multi-catch, try-with-resources.
- Java 8: Exceptions in lambdas and streams.
- Java 9+: Stack-Walking API for efficient error analysis.
- Java 14+: Helpful
NullPointerException
messages. - Java 21: Structured concurrency improves error propagation across threads.
FAQ: Expert-Level Questions
Q1. Why can’t I catch Error
?
Because errors represent unrecoverable conditions like OutOfMemoryError
.
Q2. Should I always log exceptions?
Yes, unless deliberately ignored with documented reason.
Q3. What’s the best log level for exceptions?
Use ERROR
for failures, WARN
for recoverable issues.
Q4. How to avoid duplicate logging?
Log at one layer; rethrow without logging if higher layer logs.
Q5. Should I use SLF4J over Log4j?
Yes, SLF4J is preferred as a facade—it allows switching backends.
Q6. How do I log suppressed exceptions?
Use Throwable.getSuppressed()
.
Q7. Can logging impact performance?
Yes, especially synchronous I/O—use async logging for high throughput.
Q8. What’s structured logging?
Logging in JSON or key-value pairs for machine parsing.
Q9. How to handle sensitive data in logs?
Mask or omit sensitive fields.
Q10. How does logging integrate with observability tools?
Frameworks like SLF4J integrate with ELK, Splunk, Prometheus.
Conclusion and Key Takeaways
- Logging exceptions correctly is essential for debugging and monitoring.
- Prefer SLF4J as a facade for flexibility.
- Always log stack traces, not just messages.
- Avoid anti-patterns like swallowing or duplicate logging.
By mastering logging best practices, you’ll build robust, maintainable, and production-ready Java applications.