On This Page
Same call, same JDK, two different traces. Run the reflective dispatch below on JDK 21 with default settings and Method.invoke costs you two frames. Flip one system property and it costs four, and after the seventeenth call one of those four is called GeneratedMethodAccessor1. If you're trying to filter a Java stack trace down to the part that matters, you first have to know which of these lines are plumbing and which one is your bug.
Two traces for one reflective call
Default behaviour, OpenJDK 21.0.12.1 (Ubuntu build), Linux, plain java -cp from a terminal:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:118)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at billing.Lab.dispatch(Lab.java:39)
at billing.Lab.main(Lab.java:94)
Caused by: java.lang.NumberFormatException: For input string: "12,50"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Long.parseLong(Long.java:709)
at java.base/java.lang.Long.parseLong(Long.java:832)
at billing.Lab$InvoiceParser.parseAmount(Lab.java:13)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
... 3 more
Same program with -Djdk.reflect.useDirectMethodHandle=false, which on 21 switches the old pre-JDK-18 reflection code back on:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:75)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:52)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at billing.Lab.dispatch(Lab.java:39)
at billing.Lab.main(Lab.java:94)
Caused by: java.lang.NumberFormatException: For input string: "12,50"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Long.parseLong(Long.java:709)
at java.base/java.lang.Long.parseLong(Long.java:832)
at billing.Lab$InvoiceParser.parseAmount(Lab.java:13)
... 6 more
An invoice line said 12,50 and the parser wanted 1250. The bug is a number format, and the trace is mostly about everything else.
The short read
The exception you actually care about is the Caused by. InvocationTargetException (ITE from here on) is a courier: reflection catches whatever your method threw and hands it back wrapped, because a reflective call can't know which checked exceptions the target declares. Its own trace says where reflection was when it caught the thing. That's almost never where you need to be.
So: read down to Caused by, then find the first billing. frame in that trace. Lab.java:13, parseAmount. That's the line that blew up. The caller worth opening is whoever passed "12,50" in. The ... 3 more hid those frames, but they're printed in the ITE's trace right above (dispatch, then main). Hold that thought.
I'll admit what I did first. I counted the two ... N more lines against each other, decided the old trace had "lost" frames, and went looking for them in the JDK. They weren't lost. More on that in section 5.
Break it in a few shapes
Pin the JDK: sdk install java 21.0.12-tem or any 21 build; I ran Ubuntu's OpenJDK 21.0.12.1 on two vCPUs. Save this as billing/Lab.java and keep the blank lines, because every line number in the traces depends on them.
{{LAB}}
Compile once, run each shape:
mkdir -p out && javac -d out billing/Lab.java
java -cp out billing.Lab reflect # the trace from section 1
java -cp out billing.Lab proxy # interceptor that unwraps the ITE
java -cp out billing.Lab proxy-leak # interceptor that lets the ITE through
java -cp out billing.Lab undeclared # ITE inside UndeclaredThrowableException
java -cp out billing.Lab chain # servlet-style filter chain of lambdas
java -cp out billing.Lab collapsed # my frame-collapsing printer
java -cp out billing.Lab many # 40 reflective calls, prints calls 0 and 39
Variations worth doing, each one changes the trace:
-Djdk.reflect.useDirectMethodHandle=falseon any of them. The reflection frames change from two to four. Runmanywith it and call 39 showsGeneratedMethodAccessor1.invoke(Unknown Source)instead of the nativeinvoke0. Add-Dsun.reflect.inflationThreshold=3and the switch happens at call 4 instead of call 16 (I probed this: the flip lands on the 17th call by default, counting from zero it's index 16).-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFramesonreflect. Four extra frames appear underparseAmount(LambdaForm$DMH,LambdaForm$MH,Invokers$Holder, andDirectMethodHandleAccessor.invokeImpl). Onproxyyou also get aLab$$Lambda.invoke(Unknown Source)frame. They're normally hidden. Don't leave this on anywhere you care about log volume.
Near-miss check: if your jdk.proxy1 says jdk.proxy2, or the hex address in a LambdaForm$DMH/0x... line differs, that's normal. Those numbers are assigned at runtime. If your frames say NativeMethodAccessorImpl without the flag, you're on JDK 17 or older, and that's the old path already.
IDE consoles are the usual environment trap: most can fold library frames, and I don't have one in this sandbox, so I can't tell you what yours hides. Compare with a plain terminal run before you trust it.
Reading Java stack traces through reflection, proxies and filter chains
The annotated trace below is the nastiest of the five. Interface Parser.parse declares no checked exceptions, the interceptor lets the ITE out instead of unwrapping it, and the proxy wraps that in UndeclaredThrowableException (UTE). Three exceptions, one bug.
Exception in thread "main" java.lang.reflect.UndeclaredThrowableException (1)
at jdk.proxy1/jdk.proxy1.$Proxy0.parse(Unknown Source) (2)
at billing.Lab.main(Lab.java:97) (3)
Caused by: java.lang.reflect.InvocationTargetException (4)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:118) (5)
at java.base/java.lang.reflect.Method.invoke(Method.java:580) (6)
at billing.Lab.lambda$proxy$0(Lab.java:46) (7)
... 2 more (8)
Caused by: java.lang.NumberFormatException: For input string: "12,50" (9)
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Long.parseLong(Long.java:709)
at java.base/java.lang.Long.parseLong(Long.java:832)
at billing.Lab$InvoiceParser.parseAmount(Lab.java:13) (10)
at billing.Lab$ParserHandler.parse(Lab.java:32) (11)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) (12)
... 4 more (13)
- The outermost exception, and the only one with no message. A UTE is what a proxy throws when its handler threw a checked exception the interface method didn't declare. The class name tells you a proxy was involved before you read a single frame.
$Proxy0.parse(Unknown Source). The proxy class is generated at runtime, has no source file and no line table (that's whyUnknown Source, see the unknown-source article). Thejdk.proxy1/prefix is its module. It's one of the few frames that proves the interceptor chain was in play.- Your caller:
main, line 97, the line that calledparse. In real code this is the frame to open if you want to know who handed in the bad value. - The ITE. In this trace it's the cause of the UTE, and has a
Caused byof its own. - Where the ITE was constructed, inside the JDK's reflection accessor (
:118). Not where the failure happened. Method.invoke, the public door into reflection.lambda$proxy$0is the interceptor's body (the lambda inproxy(...)). Line 46 ismethod.invoke(target, args). This is the first frame that's yours.- Two frames elided:
$Proxy0.parseandmain:97, identical to the two frames the UTE already printed. Elision compares from the bottom and stops at the first mismatch; the rule is in the Caused-by article. - The real exception. Read from here.
- The line that threw:
parseAmount,Lab.java:13. ParserHandler.parseat:32, the caller of the thing that threw. This is the frame I'd actually open to ask "why was it handed12,50".- The reflection accessor again, one frame deeper than step 5, at
:103: that's the line where it invokes the target. Same class, different line, which is why elision stopped right before it. - Four frames shared with the ITE's trace above:
Method.invoke,lambda$proxy$0,$Proxy0.parseandmain. The ITE'sinvoke:118doesn't match this trace'sinvoke:103, so elision stops there. Nothing lost.
My routine on a trace like this: skip to the last Caused by, find the first frame in my package (10), then step down one to its caller (11), then decide.
Why every layer adds a frame, and why the ITE trace is so short
An exception's frames are captured when the Throwable is constructed, not when it's thrown. That's the whole reason the ITE trace is short and unhelpful. Reflection catches your NumberFormatException in a catch block inside DirectMethodHandleAccessor.invoke, builds a brand new ITE right there (line 118 here), and throws that. The new exception's stack starts at the catch site and goes down to main. Your original exception's stack, the one with parseAmount in it, is untouched and hangs off the ITE as its cause.
I read the accessor source for this (the jdk-21+35 tag, through a summarising fetch rather than the raw file, so treat my line numbers as observed from the lab, not quoted from source). What the summary confirmed: invoke checks the receiver and argument count outside the try, calls an inner method that does the actual method-handle invoke, and wraps anything that escapes in an ITE. Argument mismatches, per that summary, go the other way: an IllegalArgumentException straight out, no ITE. So an ITE always means "the target method itself threw".
Why two reflection frames now and four before? JEP 416 reimplemented core reflection on method handles and shipped in JDK 18. Before that, the first calls went through a native stub (invoke0), and once a call site got hot enough (15 by default, observed above) the JDK generated a bytecode class called GeneratedMethodAccessorN and swapped it in behind a delegating wrapper. Two code paths, two possible traces for the same call, depending on how many times you'd called before. On the old path, the first request and the thousandth could print different reflection frames.
The ... N more difference stopped looking like lost frames once I lined the two traces up from the bottom. Old path: the ITE's top frame is invoke0 (Native Method), and the cause has the identical frame at the same spot, so the elision swallows that frame and everything under it: six frames. New path: the ITE's top frame is invoke:118 and the cause's is invoke:103. Same method, different line, so the cause prints one more frame and elision covers only three. It took me most of two evenings to stop misreading this, mostly because I kept comparing counts between traces from different implementations.
The java.base/ and jdk.proxy1/ prefixes are module names, printed since JDK 9 (I didn't re-run 8 to compare). Frames for lambda forms and method-handle plumbing are hidden by default, which is why ShowHiddenFrames makes the trace longer.
The ITE everybody reads instead of the cause
"The top frame of the trace is the bug." For an ITE it's the JDK reflection code, DirectMethodHandleAccessor.java:118 here. The top frame of an ITE is where the wrapper was built, and that's a JDK class.
"No Caused by, no root cause." In the proxy run (unwrapping interceptor) there's no ITE at all; the trace is one flat exception and the reflection frames sit in the middle of it:
Exception in thread "main" java.lang.NumberFormatException: For input string: "12,50"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Long.parseLong(Long.java:709)
at java.base/java.lang.Long.parseLong(Long.java:832)
at billing.Lab$InvoiceParser.parseAmount(Lab.java:13)
at billing.Lab$ParserHandler.handle(Lab.java:29)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at billing.Lab.lambda$proxy$0(Lab.java:46)
at jdk.proxy1/jdk.proxy1.$Proxy0.handle(Unknown Source)
at billing.Lab.main(Lab.java:95)
"The filter chain frames are noise, skip them." Not quite. In the chain run, three lambdas in a row, each followed by Chain.proceed at line 56 (filters.get(i++).apply), then proceed:57 at the end calling the real handler. The order of lambda$viaChain$1, $2, $3 going up the page is the order the filters ran in. If a filter 2 is the one that mutated the input, the trace is how you prove it. Filter chains are structure, not noise.
"Hiding framework frames makes traces readable." Sometimes it hides the bug. A config-driven proxy that sits in the wrong order, a filter that swallows a header: those bugs live in the framework frames. Filter after you've decided the culprit is yours, not before.
"UndeclaredThrowableException means a bug in the proxy library." The Javadoc says it's thrown when the handler throws a checked exception that no throws clause covers. The Caused by is what your handler threw. Here, an ITE that the interceptor forgot to unwrap.
Filtering a Java stack trace: IDE, logger, your own code, and the price
Four places you can cut frames, from least to most dangerous.
In the IDE. Free, reversible, and local to your screen. Fold java.base and reflection frames while you read; unfold when the culprit is nowhere in your package.
In your logging config. Both Logback and Log4j2 can shorten traces. From the Logback manual: %ex{short} prints the first line only, %ex{N} the first N lines, %rEx prints the root cause first. Log4j2's pattern layout has %ex{filters(pkg,...)}, which suppresses frames from the listed packages and leaves a marker count, plus short, depth, and %rEx. I did not run either logger this session (Maven Central was unreachable from my sandbox), so I'm reporting the documented syntax, not output. I couldn't find a rootCauseFirst option in the Logback page I read, so I've left it out. If you adopt a filter, test it on a real chained trace first and make sure the Caused by survives.
In your own code. My printCollapsed helper in the lab collapses runs of jdk.internal.reflect, java.lang.reflect, jdk.proxy and java.lang.invoke frames into [N framework frames hidden]. Output for the reflect case:
java.lang.reflect.InvocationTargetException
[2 framework frames hidden]
at billing.Lab.dispatch(Lab.java:39)
at billing.Lab.main(Lab.java:100)
Caused by: java.lang.NumberFormatException: For input string: "12,50"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Long.parseLong(Long.java:709)
at java.base/java.lang.Long.parseLong(Long.java:832)
at billing.Lab$InvoiceParser.parseAmount(Lab.java:13)
[2 framework frames hidden]
at billing.Lab.dispatch(Lab.java:39)
at billing.Lab.main(Lab.java:100)
Note it reprinted dispatch and main under the cause: my helper uses getStackTrace(), which returns the full stack for each exception and doesn't do ... N more. Prettier, not more correct. It also needs a real getCause loop with cycle protection if you ship it. Don't ship it. It's a lab toy.
By unwrapping early. The cheapest "filter" is the one the proxy interceptor does: catch the ITE where you call Method.invoke and rethrow e.getCause(). The ITE and its frames disappear, and the trace you get reads like a direct call. The price: you lose the line where reflection was invoked (lambda$proxy$0), so keep that frame in the message or log it once if the dispatch site matters. And a rethrown checked cause might force you into UndeclaredThrowableException territory in a proxy (that's the undeclared run).
What survives in the logs, and what 21 prints versus older JDKs
Production log pipelines cut traces by line count and by character count, and reflection noise eats the budget. A four-frame ITE preamble is nothing. A deep interceptor chain is another story: if your aggregator caps lines per event, the cap can land before your Caused by and you keep only the outer exception. Pull the root cause into its own field (class, message, first frame in your package) and fingerprint on that, not on the top frame.
Version notes, strictly from what I ran: on OpenJDK 21.0.12.1 default reflection prints DirectMethodHandleAccessor and Method.invoke, two frames. -Djdk.reflect.useDirectMethodHandle=false gives the older four-frame shape, then GeneratedMethodAccessor1 after the inflation threshold. JEP 416 says the flag is a compatibility switch that will stop working once the old implementation is removed, so don't build tooling on it. I did not run JDK 8, 11, 17 or 25, so what happens there is the JEP and the flag, not a pasted run. Anything that parses frame names with a regex (sun.reflect. on 8, jdk.internal.reflect. on 9+, then the accessor class names) will drift across versions.
If you want the live view of the same frames rather than a printed trace, StackWalker hides reflection frames by default and has an option to show them. That's for the StackWalker article. For wrapping habits that make all of this easier, see writing exceptions with useful traces and logging exceptions properly. If you're starting from zero on trace anatomy, the hub article is the place. And for an ITE seen from the application side (null message, real cause), there's already this post on InvocationTargetException.
Keep these
InvocationTargetExceptionandUndeclaredThrowableExceptionare couriers. Read theirCaused by, not their top frame.- The ITE's top frame is where reflection built the wrapper (
DirectMethodHandleAccessor:118on 21), not where your code failed. - Reflection costs two frames on JDK 18+ and four on older paths; the old path swaps to
GeneratedMethodAccessorNafter about 15 calls, so the same call can print differently. - Filter chain and interceptor frames are the execution order. Hide them last.
- Unwrap the ITE at the call site if you can. It's a one-line fix and the trace reads like a direct call.
Questions from the search box
What is jdk.internal.reflect.DirectMethodHandleAccessor in my stack trace?
It's the JDK's reflection accessor since JDK 18 (JEP 416). It invokes your method through a method handle and wraps anything thrown in an InvocationTargetException. The frame is plumbing; look under Caused by.
Why is my InvocationTargetException message null?
Reflection builds it with the target exception as the cause and no message. It carries no text of its own on JDK 21, as in the traces above. Use getCause(), which the Javadoc recommends over getTargetException().
What does UndeclaredThrowableException mean?
A proxy's invocation handler threw a checked exception that the interface method doesn't declare. Whatever your handler threw is the cause. Fix it by declaring the exception, or by unwrapping and rethrowing something the method allows.
How do I hide Spring or reflection frames in logs?
Log4j2: %ex{filters(org.springframework)}; Logback has depth options (%ex{20}) and %rEx for root-cause-first. Both are from the docs, not a run of mine. Filtering hides evidence, so keep unfiltered traces somewhere for the days when the bug is in the framework layer.