Stack Trace
A stack trace is the recorded sequence of method calls that were active at the moment an exception was thrown. It is the most important debugging tool in Java — a precise map from the exception back through every method call that led to it, with file names and line numbers at each level. Understanding how to read a stack trace, how the JVM records it, what information each line conveys, how to work with stack traces programmatically, and how to produce clean stack traces in production code is a foundational skill for every Java developer. This entry covers stack trace structure, reading nested and chained traces, programmatic access and manipulation, logging strategies, stack trace filtering, and common patterns for diagnosing real-world problems from stack traces.
Stack Trace Structure and How to Read It
// ── Reading a stack trace ────────────────────────────────────────────
//
// Exception in thread "main" java.lang.NullPointerException: ← type + message
// Cannot invoke "String.length()" because "str" is null ← Java 14+ helpful NPE
// at com.myapp.StringUtils.process(StringUtils.java:42) ← THROWN HERE (line 42)
// at com.myapp.OrderService.formatOrder(OrderService.java:87) ← called process()
// at com.myapp.OrderController.getOrder(OrderController.java:53) ← called formatOrder()
// at com.myapp.OrderController$$EnhancerBySpring.getOrder(Unknown Source) ← proxy
// at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ← JVM internals
// at org.springframework.web.servlet.FrameworkServlet.service(...) ← framework
// at java.lang.Thread.run(Thread.java:834) ← thread entry point
//
// Key observations:
// 1. Line 1: NullPointerException — str is null in process()
// 2. Line 2: process() at line 42 in StringUtils — thrown here
// 3. Line 3: formatOrder() called process() with null str
// 4. Line 4: getOrder() called formatOrder()
// 5. The bug is likely in getOrder() passing null to formatOrder()
// ── What each StackTraceElement contains ─────────────────────────────
try {
throw new RuntimeException("demo");
} catch (RuntimeException e) {
StackTraceElement[] frames = e.getStackTrace();
StackTraceElement top = frames[0]; // where exception was thrown
System.out.println(top.getClassName()); // fully qualified class
System.out.println(top.getMethodName()); // method name
System.out.println(top.getFileName()); // source file name
System.out.println(top.getLineNumber()); // line number (or -1 if unknown)
System.out.println(top.isNativeMethod()); // true for native methods
System.out.println(top.getModuleName()); // module name (Java 9+, may be null)
}
// ── Stack frame patterns to recognise ────────────────────────────────
// Native method (no line number):
// at java.io.FileInputStream.read0(Native Method)
//
// Unknown source (no debug info):
// at com.obfuscated.Lib.method(Unknown Source)
//
// JVM-generated proxy class:
// at com.myapp.Service$$EnhancerByCGLIB$$abc123.method(...)
//
// Lambda expression:
// at com.myapp.Service.lambda$processOrder$0(Service.java:55)
//
// First find YOUR code — that is where the real investigation startsProgrammatic Stack Trace Access
// ── Access stack trace from exception ────────────────────────────────
public static String formatStackTrace(Throwable e) {
StringBuilder sb = new StringBuilder();
sb.append(e.getClass().getName())
.append(": ")
.append(e.getMessage())
.append('
');
for (StackTraceElement frame : e.getStackTrace()) {
sb.append(" at ")
.append(frame.getClassName())
.append('.')
.append(frame.getMethodName())
.append('(')
.append(frame.getFileName())
.append(':')
.append(frame.getLineNumber())
.append(')')
.append('
');
}
return sb.toString();
}
// ── Filter stack trace — show only application frames ─────────────────
public static String appStackTrace(Throwable e,
String appPackage) {
StringBuilder sb = new StringBuilder();
sb.append(e.getClass().getSimpleName())
.append(": ")
.append(e.getMessage())
.append('
');
for (StackTraceElement frame : e.getStackTrace()) {
if (frame.getClassName().startsWith(appPackage)) {
sb.append(" at ").append(frame).append('
');
}
}
return sb.toString();
}
// Usage: appStackTrace(e, "com.myapp") — only shows com.myapp frames
// ── Get current thread's stack without an exception ───────────────────
public static String currentCallStack() {
StackTraceElement[] frames =
Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder("Call stack:
");
for (int i = 2; i < frames.length; i++) { // skip getStackTrace and this method
sb.append(" ").append(frames[i]).append('
');
}
return sb.toString();
}
// ── Suppress stack trace for control-flow exceptions ─────────────────
// Constructing a stack trace is expensive (~microseconds)
// For exceptions used as signals rather than errors, suppress it:
public class RetrySignal extends RuntimeException {
public RetrySignal(String reason) {
super(reason,
null, // cause
true, // enableSuppression
false); // writableStackTrace = false → no stack capture
}
}
// The 4-arg Throwable constructor controls stack trace capture
// Use when: exception is expected, frequent, and message is sufficient
// Do NOT use for genuine errors — debugging becomes impossible
// ── Stack trace to string for logging ─────────────────────────────────
public static String stackTraceToString(Throwable t) {
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw, true));
return sw.toString();
}
// Preferred over e.printStackTrace() in production —
// that writes to System.err, not your logging frameworkLogging Stack Traces Correctly
// ── Correct logging with SLF4J ────────────────────────────────────────
@Service
@Slf4j
public class OrderService {
public void processOrder(Long orderId) {
try {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
processOrderItems(order);
chargePayment(order);
} catch (OrderNotFoundException e) {
// Expected — WARN level, no stack trace needed (message is enough)
log.warn("Order not found for processing: orderId={}", orderId);
throw e;
} catch (PaymentDeclinedException e) {
// Business failure — WARN level with minimal context
log.warn("Payment declined: orderId={} reason={}",
orderId, e.getDeclineReason());
throw e;
} catch (Exception e) {
// Unexpected — ERROR level with full stack trace
// Pass exception as LAST argument — SLF4J renders full trace
log.error("Unexpected error processing order: orderId={}",
orderId, e);
throw new ServiceException(
"Order processing failed: " + orderId, e);
}
}
}
// ── WRONG patterns ────────────────────────────────────────────────────
catch (Exception e) {
e.printStackTrace(); // WRONG — System.err, bypasses logging framework
}
catch (Exception e) {
log.error(e.getMessage()); // WRONG — no stack trace, just the message
}
catch (Exception e) {
log.error("Error: " + e); // WRONG — calls toString(), no stack trace
}
// ── CORRECT ────────────────────────────────────────────────────────────
catch (Exception e) {
log.error("Order processing failed: orderId={}", orderId, e);
// SLF4J: message with params + exception as last arg = full stack trace
}
// ── Logback configuration — filter noisy frames ───────────────────────
// logback-spring.xml — show only first N frames from application packages:
// <configuration>
// <conversionRule conversionWord="ex"
// converterClass="ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter"/>
// <appender name="CONSOLE" class="...ConsoleAppender">
// <encoder>
// <pattern>%d %-5p %logger{36} : %m%n%ex{5,
// com.myapp,
// OMIT:org.springframework,
// OMIT:org.hibernate,
// OMIT:com.sun,
// OMIT:java.lang.reflect
// }</pattern>
// </encoder>
// </appender>
// </configuration>Stack Trace Analysis — Diagnosing Real Problems
// ── Pattern 1: NullPointerException — Java 14+ helpful message ────────
// java.lang.NullPointerException:
// Cannot invoke "com.myapp.Order.getTotal()" because "order" is null
// at com.myapp.OrderService.calculateTax(OrderService.java:78)
//
// Diagnosis: order is null at line 78 in calculateTax()
// Fix: trace where order comes from — it was either not initialised
// or findById() returned null and wasn't checked
// ── Pattern 2: StackOverflowError — infinite recursion ────────────────
// java.lang.StackOverflowError
// at com.myapp.TreeNode.calculateDepth(TreeNode.java:23)
// at com.myapp.TreeNode.calculateDepth(TreeNode.java:23)
// at com.myapp.TreeNode.calculateDepth(TreeNode.java:23)
// ... (same frame repeated thousands of times) ...
//
// Diagnosis: calculateDepth() calls itself without a base case
// Fix: add termination condition (if node == null return 0)
// ── Pattern 3: ClassCastException ────────────────────────────────────
// java.lang.ClassCastException:
// class java.lang.String cannot be cast to class java.lang.Integer
// at com.myapp.ConfigService.getIntValue(ConfigService.java:45)
//
// Diagnosis: a config value stored as String is being cast to Integer
// Fix: use Integer.parseInt() or add type checking
// ── Pattern 4: ConcurrentModificationException ────────────────────────
// java.util.ConcurrentModificationException
// at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
// at java.util.ArrayList$Itr.next(ArrayList.java:861)
// at com.myapp.UserService.removeInactive(UserService.java:67)
//
// Diagnosis: list modified while iterating at line 67
// Fix: use Iterator.remove(), removeIf(), or collect removals first
// ── Analysing async stack traces ──────────────────────────────────────
// CompletableFuture exception — trace starts at thread pool:
// java.lang.RuntimeException: processing failed
// at com.myapp.AsyncService.process(AsyncService.java:42) ← async code
// at java.util.concurrent.CompletableFuture.uniApply(...)
// at java.util.concurrent.ForkJoinTask.doExec(...)
// at java.util.concurrent.ForkJoinPool.runWorker(...)
// at java.util.concurrent.ForkJoinWorkerThread.run(...)
//
// Notice: NO frame showing who submitted the task — stack starts at thread pool
// Use MDC correlation IDs to link async work to the originating request
// ── Capturing context for async correlation ───────────────────────────
@Async
public CompletableFuture<Result> processAsync(Request req) {
// MDC values from the calling thread should be propagated:
// (Configure TaskDecorator in ThreadPoolTaskExecutor to copy MDC)
log.info("Processing async: requestId={}", req.getId());
try {
return CompletableFuture.completedFuture(process(req));
} catch (Exception e) {
log.error("Async processing failed: requestId={}", req.getId(), e);
throw e;
}
}