A high-throughput lookup service throws `NotFoundException` for an ordinary "no such record" outcome, thousands of times a second under normal load. Profiling shows a meaningful chunk of CPU going into exception construction, not into the throw itself. What's actually expensive here, and how would you fix it without changing the exception's meaning?
"Exceptions are expensive" is a half-truth that points at the wrong thing — the actual athrow and stack unwinding are cheap, ordinary control flow, comparable to a return from deep in a call chain. What's genuinely expensive is fillInStackTrace(), a native method that runs inside every Throwable's constructor, before the throw even happens: it walks the entire current call stack, from the point of construction back to the thread's entry point, and records every frame's class, method, file and line. That walk happens on every single new NotFoundException(...), whether or not anyone ever calls getStackTrace() on it afterward — and for a validation-style exception like "no such record," thrown from the same handful of lines every time, the trace tells the log reader nothing beyond what the exception's type and message already say. The fix, without changing what the exception means or how it's caught: have this specific, high-volume, low-diagnostic-value exception type skip stack trace capture entirely, via Throwable's protected four-argument constructor with writableStackTrace = false — not truncated, genuinely empty, and genuinely free of the walk that was costing the CPU.