Back to Exception Chaining & Stack Traces
Q4Question 4 of 4

What is fillInStackTrace() and why are exceptions expensive?

Key Concepts
  • fillInStackTrace() is called by every exception constructor to capture the full call stack, costing 1 to 5 microseconds
  • The cost comes from a native call that walks every stack frame and creates StackTraceElement objects for each one
  • Override fillInStackTrace() to return this for stackless exceptions that are about 100 times faster to create
  • Use stackless exceptions on hot paths where the throw point is known and stack traces are not needed for debugging
  • Pre-built singleton exceptions eliminate object allocation entirely: static final INSTANCE = new StacklessException()
  • Keep normal stack traces for rare, unexpected errors where line numbers are essential for debugging

Definition

fillInStackTrace() captures the full call stack on every exception creation, costing 1 to 5 microseconds, and can be overridden to return this for stackless exceptions that are 100 times cheaper.
  • fillInStackTrace() is called internally by every Throwable constructor to capture the current call stack. It is a native method that walks the JVM stack frame by frame, creating StackTraceElement objects 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 return this without 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.

JAVA
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() + ")");
        }
    }
}
TEXT
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.

JAVA
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");
    }
}
TEXT
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.

JAVA
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);
        }
    }
}
TEXT
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.

JAVA
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");
    }
}
TEXT
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate