Definition
fillInStackTrace()is called internally by everyThrowableconstructor to capture the current call stack. It is a native method that walks the JVM stack frame by frame, creatingStackTraceElementobjects for each frame.- Cost: roughly 1 to 5 microseconds per exception. For a 20-frame deep stack, that means 20 StackTraceElement objects allocated.
- Stackless exception: override
fillInStackTrace()to returnthiswithout capturing. This skips the stack walk and is roughly 100 times cheaper. - Pre-built singleton: combine stackless with a static final instance for zero allocation per throw.
- Use when: the exception is on a hot path (thousands per second), the throw point is known, and the stack trace is not needed for debugging.
Every time a teller at National Bank of India raises an alarm, they must write a full incident report listing everyone involved (stack trace). For a rare fire, this is worth the effort. For "customer entered wrong PIN" happening 10,000 times per day, writing a full report each time wastes resources. Fix: for frequent expected events, use a simple alert (stackless exception). Save full reports for unexpected incidents.
Understanding the Concepts
What does fillInStackTrace() do?
Every time you create an exception with new Exception(), the constructor calls fillInStackTrace(). This native method walks the current thread's stack and creates a StackTraceElement array.
package com.bank.demo;
public class FillInStackTraceDemo {
public static void main(String[] args) {
// Every new Exception() calls fillInStackTrace internally
Exception ex = new Exception("Test");
StackTraceElement[] trace = ex.getStackTrace();
System.out.println("Stack frames captured: " + trace.length);
for (StackTraceElement frame : trace) {
System.out.println(" " + frame.getClassName() + "." + frame.getMethodName()
+ "(" + frame.getFileName() + ":" + frame.getLineNumber() + ")");
}
}
}
Stack frames captured: 1
com.bank.demo.FillInStackTraceDemo.main(FillInStackTraceDemo.java:6)
Why is stack trace capture expensive?
The cost comes from the native stack walk and object allocation. For hot paths where exceptions are thrown thousands of times per second, this overhead matters.
package com.bank.demo;
public class ExceptionCostDemo {
static class NormalException extends RuntimeException {
NormalException(String msg) { super(msg); }
}
static class StacklessException extends RuntimeException {
StacklessException(String msg) { super(msg); }
@Override public synchronized Throwable fillInStackTrace() {
return this; // skip stack capture
}
}
public static void main(String[] args) {
int iterations = 100_000;
// Normal exception: with stack trace
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
try { throw new NormalException("test"); }
catch (NormalException e) { }
}
long normalMs = (System.nanoTime() - start) / 1_000_000;
// Stackless exception: without stack trace
start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
try { throw new StacklessException("test"); }
catch (StacklessException e) { }
}
long stacklessMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("Normal (with trace): " + normalMs + "ms");
System.out.println("Stackless (no trace): " + stacklessMs + "ms");
System.out.println("Speedup: ~" + (normalMs / Math.max(stacklessMs, 1)) + "x");
}
}
Normal (with trace): 180ms
Stackless (no trace): 5ms
Speedup: ~36x
How do you create a stackless exception?
Override fillInStackTrace() to return this without capturing. Optionally create a static singleton for zero allocation.
package com.bank.service;
public class ValidationException extends RuntimeException {
// Override: skip stack trace capture
@Override
public synchronized Throwable fillInStackTrace() {
return this; // no stack walk, 100x faster
}
// Pre-built singleton for zero allocation per throw
public static final ValidationException INVALID_AMOUNT =
new ValidationException("Amount must be positive");
public static final ValidationException INVALID_ACCOUNT =
new ValidationException("Account ID must not be blank");
public ValidationException(String message) {
super(message);
}
public static void main(String[] args) {
try {
throw ValidationException.INVALID_AMOUNT;
} catch (ValidationException e) {
System.out.println("Caught: " + e.getMessage());
System.out.println("Stack frames: " + e.getStackTrace().length);
}
}
}
Caught: Amount must be positive
Stack frames: 0
When should you use stackless exceptions?
Use stackless when the exception is expected, frequent, and the throw point is known. Keep stack traces for rare, unexpected errors.
package com.bank.demo;
public class WhenToUseStackless {
// USE stackless: validation on hot path, throw point known
// - Cache miss exceptions (10K+ per second)
// - Input validation failures (known check point)
// - Rate limiting rejections (expected, frequent)
// KEEP stack traces: rare, unexpected errors
// - NullPointerException (need line number to find bug)
// - ClassCastException (need to know where the bad cast is)
// - Database errors (need full trace for debugging)
public static void main(String[] args) {
System.out.println("Stackless: hot path, known throw point, expected");
System.out.println(" - Cache miss, validation, rate limit");
System.out.println();
System.out.println("Keep traces: rare, unexpected, need debugging");
System.out.println(" - NPE, ClassCast, DB errors");
}
}
Stackless: hot path, known throw point, expected
- Cache miss, validation, rate limit
Keep traces: rare, unexpected, need debugging
- NPE, ClassCast, DB errors
What should a senior engineer know?
- Netty uses stackless exceptions. High-performance frameworks like Netty override fillInStackTrace for exceptions used in channel pipelines.
- JVM optimization. The JIT compiler can optimize frequently thrown exceptions by caching the stack trace, but overriding fillInStackTrace is still more reliable.
- Thread safety. fillInStackTrace is synchronized in the Throwable class. Overriding it avoids the synchronization overhead too.
- -XX:-OmitStackTraceInFastThrow. The JVM may omit stack traces for frequently thrown exceptions after initial occurrences. Add this flag to force full traces.
What are common interview follow-up questions?
- What makes exception creation expensive? (fillInStackTrace native call walking the stack.)
- How much faster is a stackless exception? (Roughly 30 to 100 times depending on stack depth.)
- Can you have a stackless exception with a cause? (Yes, the cause chain still works.)
- What is the JVM flag for forcing stack traces? (
-XX:-OmitStackTraceInFastThrow)
Visual Overview
new Exception("msg")
│
▼
constructor calls fillInStackTrace()
│
▼
Native method walks thread stack
│
▼
Creates StackTraceElement for each frame
(class, method, file, line)
│
▼
Cost: 1-5 microseconds
For 100K throws: 100-500ms overhead
Override fillInStackTrace() { return this; }
│
▼
No stack walk. No allocation.
Cost: ~0.05 microseconds
100x cheaper.
Key Takeaway
Every exception constructor calls fillInStackTrace(), a native method that walks the entire call stack and creates StackTraceElement objects for each frame. This costs 1 to 5 microseconds per exception. For exceptions thrown thousands of times per second on hot paths (validation failures, cache misses), the overhead is significant: 100,000 throws at 2 microseconds each consumes 200 milliseconds of CPU.
Override fillInStackTrace() to return this for stackless exceptions that are roughly 100 times cheaper to create. Combine with pre-built singleton instances (static final INSTANCE = new StacklessException()) for zero allocation per throw. Use stackless when the exception is expected, the throw point is known, and stack traces are not needed for debugging.
Keep normal stack traces for rare, unexpected errors like NullPointerException and ClassCastException where line numbers are essential for finding the bug. In the rate-limiting service at National Bank of India, Pankaj uses a stackless RateLimitException.INSTANCE thrown 50,000 times per second during peak trading hours, saving hundreds of milliseconds of CPU per second.
Remember one sentence: override fillInStackTrace to return this for hot-path exceptions where the throw point is known and debugging traces are not needed.
---