> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Strange Java Stack Frames: lambda$0, $1, init and Proxies
- URL: https://www.ggorantala.dev/java-stack-trace-strange-frames/
- Published: 2026-09-12T01:07:00.000Z
- Updated: 2026-11-01T02:14:12.000Z
- Description: A lambda$main$0 stack trace, access$000, <init>, $Proxy0: what odd frame names in a Java stack trace mean, where javac puts them, and how to read past them.
- Author: Gopi Gorantala
- Tags: stack-traces, jvm-internals

## Line 7 of a file where nothing happens on line 7

OpenJDK 21.0.12.1, Linux, a small rule evaluator. I compiled it with `--release 8` (more on why in a minute) and ran it with plain `java -cp out8 RuleLab inner` from the console. This came out, copied as-is:

```log
java.lang.IllegalStateException: secret A-17
	at RuleLab.secret(RuleLab.java:40)
	at RuleLab.access$000(RuleLab.java:7)
	at RuleLab$Peeker.peek(RuleLab.java:45)
	at RuleLab.viaInner(RuleLab.java:73)
	at RuleLab.main(RuleLab.java:95)

```

Line 7 of `RuleLab.java` is `public class RuleLab {`. Nothing executes on line 7\. There's also a method called `access$000` that I never wrote, and it's sitting in the middle of my own call chain.

Here's a second one, from the same program, a lambda this time. If you've pasted a `lambda$main$0` stack trace into a search box, this is the family you're in:

```log
java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)
	at RuleLab.lambda$viaLambda$0(RuleLab.java:50)
	at java.base/java.util.Arrays$ArrayList.forEach(Arrays.java:4305)
	at RuleLab.viaLambda(RuleLab.java:51)
	at RuleLab.main(RuleLab.java:91)

```

`lambda$viaLambda$0` isn't in the source either. It *is* yours, though, and `access$000` isn't. Telling those two apart on sight is most of this article. The rest of it is `$1`, `<init>`, `<clinit>`, doubled frames and `$Proxy0`. If you haven't read the hub post, [how to read a Java stack trace](https://ggorantala.dev/reading-java-stack-traces/?ref=ggorantala.dev) covers the frame format I'm assuming here.

## The short read

Every odd name in a trace comes from one of three places: javac generated a method or class for you, the JVM named a special method (`<init>`, `<clinit>`), or the runtime generated a whole class at run time (a proxy). None of them is a bug. Each one tells you something about how the call got from A to B.

My first instinct on the doubled `check` frame you'll meet below was recursion, so I went looking for a method calling itself. There isn't one. On `access$000(RuleLab.java:7)` I assumed a broken line table. Also wrong; I'll show where line 7 comes from.

Which frame would I open? In the lambda trace, `lambda$viaLambda$0` at line 50, because that's the body you wrote. In the `access$000` trace, I'd skip it and open `secret` at line 40, or `peek` at line 45.

## Make this trace yourself

Pinned to OpenJDK 21.0.12.1 (the Ubuntu 24.04 package). Any JDK 21 should print the same shape. I did not run JDK 8, 17 or 25 runtimes, so every trace in here is JDK 21 output.

Save this as `RuleLab.java`. Keep the blank lines and the line breaks. Every line number in the traces depends on them. It's written to compile with `--release 8` as well, which is why there's no record and no arrow-switch in it.

```java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class RuleLab {
    interface Rule<T> {
        void check(T order) throws Exception;
    }

    static class Order {
        final String id; final int qty;
        Order(String id, int qty) { this.id = id; this.qty = qty; }
        String id() { return id; }
        void validate() {
            if (qty <= 0) throw new IllegalStateException("qty " + qty + " on " + id);
        }
    }

    static class QtyRule implements Rule<Order> {
        public void check(Order o) {
            o.validate();
        }
    }

    static class Threshold {
        final int limit;
        Threshold(int limit) {
            if (limit < 0) throw new IllegalArgumentException("limit " + limit);
            this.limit = limit;
        }
    }

    static class Defaults {
        static final int MAX = Integer.parseInt(System.getProperty("max", "x12"));
    }

    private String secret(Order o) {
        throw new IllegalStateException("secret " + o.id());
    }

    class Peeker {
        String peek(Order o) {
            return secret(o);
        }
    }

    static void viaLambda(List<Order> orders) {
        Consumer<Order> c = o -> o.validate();
        orders.forEach(c);
    }

    static void viaMethodRef(List<Order> orders) {
        orders.forEach(Order::validate);
    }

    static void viaAnon(Order o) throws Exception {
        Rule<Order> r = new Rule<Order>() {
            public void check(Order x) {
                x.validate();
            }
        };
        r.check(o);
    }

    static void viaBridge(Order o) throws Exception {
        Rule raw = new QtyRule();
        raw.check(o);
    }

    static void viaInner(Order o) {
        new RuleLab().new Peeker().peek(o);
    }

    static void viaProxy(Order o) throws Exception {
        InvocationHandler h = (proxy, m, a) -> {
            new QtyRule().check((Order) a[0]);
            return null;
        };
        @SuppressWarnings("unchecked")
        Rule<Order> r = (Rule<Order>) Proxy.newProxyInstance(
            RuleLab.class.getClassLoader(), new Class<?>[] { Rule.class }, h);
        r.check(o);
    }

    public static void main(String[] args) throws Exception {
        Order bad = new Order("A-17", 0);
        try {
            String w = args[0];
            if (w.equals("lambda")) viaLambda(Arrays.asList(bad));
            else if (w.equals("mref")) viaMethodRef(Arrays.asList(bad));
            else if (w.equals("anon")) viaAnon(bad);
            else if (w.equals("bridge")) viaBridge(bad);
            else if (w.equals("inner")) viaInner(bad);
            else if (w.equals("proxy")) viaProxy(bad);
            else if (w.equals("init")) new Threshold(-1);
            else if (w.equals("clinit")) System.out.println(Defaults.MAX);
            else throw new IllegalArgumentException(w);
        } catch (Throwable t) {
            t.printStackTrace(System.out);
        }
    }
}

```

Steps:

1. Compile normally: `javac -d out21 RuleLab.java`. You'll get a note about unchecked operations. That's the raw `Rule` in `viaBridge`, on purpose.
2. Run the lambda one: `java -cp out21 RuleLab lambda`. You should get exactly the second trace above. Line numbers match if you copied the file exactly.
3. Run the rest: `lambda`, `mref`, `anon`, `bridge`, `inner`, `proxy`, `init`, `clinit`. I print each below, annotated.
4. Compile for an old target: `javac --release 8 -Xlint:-options -d out8 RuleLab.java`, then `java -cp out8 RuleLab inner`. That's the first trace in the article. This is JDK 21 running version-52 class files. It is not a JDK 8 runtime, and I want to be clear about that.
5. Show the frames the JVM hides: `java -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -cp out21 RuleLab lambda`.

Near-miss: if `inner` prints no `access$000` frame, you compiled without `--release 8`. That's the point of the next sections, not a mistake.

A second file covers the names the first one doesn't. Same rules, keep the line layout (the comments say which line matters):

```java
import java.util.function.Supplier;

public class Names {
    static final Supplier<String> S = () -> boom("static");          // line 4
    final Supplier<String> f = () -> boom("field");                  // line 5

    enum Mode {
        FAST { String run() { return boom("enum"); } },              // line 8
        SLOW { String run() { return "ok"; } };
        abstract String run();
    }

    static String boom(String where) {
        throw new IllegalStateException(where);                      // line 14
    }

    static String nested() {
        Supplier<Supplier<String>> outer = () -> () -> boom("nested"); // line 18
        return outer.get().get();
    }

    static String local() {
        class Local { String go() { return boom("local"); } }        // line 23
        return new Local().go();
    }

    static void show(Runnable r) {
        try { r.run(); } catch (Throwable t) { t.printStackTrace(System.out); }
    }

    public static void main(String[] args) {
        show(() -> S.get());
        show(() -> new Names().f.get());
        show(() -> Mode.FAST.run());
        show(() -> nested());
        show(() -> local());
    }
}

```

Compile it with `javac -d outN Names.java`, run `java -cp outN Names`. And a small proxy file, `P.java`:

```java
import java.lang.reflect.*;
public class P {
    public static void main(String[] a) {
        Runnable r = (Runnable) Proxy.newProxyInstance(P.class.getClassLoader(), new Class<?>[]{Runnable.class},
            (p, m, x) -> { throw new IllegalStateException("from handler"); });
        try { r.run(); } catch (Throwable t) { t.printStackTrace(System.out); }
        Callable2 c = (Callable2) Proxy.newProxyInstance(P.class.getClassLoader(), new Class<?>[]{Callable2.class},
            (p, m, x) -> { throw new java.io.IOException("checked"); });
        try { c.go(); } catch (Throwable t) { t.printStackTrace(System.out); }
    }
    public interface Callable2 { void go(); }
}

```

`javac -d outP P.java && java -cp outP P`. IDE consoles tend to fold the JDK frames, so run these from a terminal the first time.

## `lambda$main$0` and friends: every odd frame, decoded

### Lambdas and method references

```log
java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)                  (1)
	at RuleLab.lambda$viaLambda$0(RuleLab.java:50)              (2)
	at java.base/java.util.Arrays$ArrayList.forEach(Arrays.java:4305)  (3)
	at RuleLab.viaLambda(RuleLab.java:51)                       (4)
	at RuleLab.main(RuleLab.java:91)

```

(1) is where the exception was created, your real code. (2) is the lambda body: javac turned `o -> o.validate()` into a private static method named `lambda$` \+ the enclosing method + `$` \+ a number. The line is the line inside the lambda. (3) is the JDK calling your lambda, on the `java.base` module. (4) is the method that *created* the lambda and handed it to `forEach`, and line 51 is the `forEach` call.

Notice what's missing between (2) and (3): the object that implements `Consumer`. It exists, and I'll get to it. The JVM hides it.

Now the method reference. Here it is, same job, `orders.forEach(Order::validate)`:

```log
java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)
	at java.base/java.util.Arrays$ArrayList.forEach(Arrays.java:4305)
	at RuleLab.viaMethodRef(RuleLab.java:55)
	at RuleLab.main(RuleLab.java:92)

```

No `lambda$` frame at all. A method reference to an existing method doesn't need a synthetic body, so the trace is one frame shorter and has no generated name in it.

### The number after the second dollar sign

That trailing `0` surprised me more than anything else in this lab. From `Names.java`:

```log
java.lang.IllegalStateException: static
	at Names.boom(Names.java:14)
	at Names.lambda$static$0(Names.java:4)
	at Names.lambda$main$4(Names.java:32)
	at Names.show(Names.java:28)
	at Names.main(Names.java:32)

```

The lambda in `main` is `lambda$main$4`. Not `$0`. The counter is per class, not per method, and `main` has five lambdas that come after the ones above it. The rest of the run, with the `boom` frame on top and everything below the interesting frame (`lambda$main$N`, `show`, `main`) cut by me:

```log
	at Names.lambda$new$1(Names.java:5)        <- lambda in an instance field initializer
	at Names$Mode$1.run(Names.java:8)          <- enum constant with a body
	at Names.lambda$nested$2(Names.java:18)    <- the INNER of () -> () -> boom("nested")
	at Names$1Local.go(Names.java:23)          <- a local class

```

The field initializer's lambda is named after the constructor (`new`), a static one after `static`. The inner lambda of a nested pair got `$2` and the outer `$3` (I checked with `javap -p`), so inner before outer. And I added one throwaway lambda to line 4 of a scratch copy, on the same line so nothing else moved. Every later name shifted by one: `lambda$static$1`, `lambda$new$2`, `lambda$main$5`. Same lines, different names.

### Anonymous, inner, local, and the doubled frame

```log
java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)
	at RuleLab$1.check(RuleLab.java:61)                          (1)
	at RuleLab$1.check(RuleLab.java:59)                          (2)
	at RuleLab.viaAnon(RuleLab.java:64)
	at RuleLab.main(RuleLab.java:93)

```

`RuleLab$1` is the first anonymous class in `RuleLab`; the number is again assigned by javac, in order. `Names$1Local` above is the same scheme for a local class (the digit comes before the name), and `Names$Mode$1` is an enum constant with a body.

And now the doubled `check`. (1) is your method, line 61\. (2) is a *bridge method*: the interface says `check(Object)` after erasure, your class implements `check(Order)`, so javac generates a `check(Object)` that casts and calls the real one. The frame points at line 59, the line where the class starts. Same story for `QtyRule` in the `bridge` run, lines 23 and 21\. I confirmed with `javap -p -v` that the generated method has `ACC_BRIDGE, ACC_SYNTHETIC` and a `checkcast` followed by the real call.

### `access$000`, and the line 7 mystery

With `--release 8` classes, `Peeker` (an inner class) calls a *private* method of `RuleLab`. Before Java 11 the JVM had no way to allow that, so javac generated a package-private static method on the outer class, `access$000(RuleLab, Order)`, and routed the call through it. `javap` shows it as `static` and `ACC_SYNTHETIC`. Its line number is the line of the outer class declaration: 7\. That's your answer to line 7\. With the default `javac` on 21 the frame is gone, because nested classes became *nestmates* in Java 11 ([JEP 181](https://openjdk.org/jeps/181?ref=ggorantala.dev)) and `javap -v` on the compiled classes shows the `NestHost` and `NestMembers` attributes instead.

### `<init>` and `<clinit>`

```log
java.lang.IllegalArgumentException: limit -1
	at RuleLab$Threshold.<init>(RuleLab.java:30)
	at RuleLab.main(RuleLab.java:97)

```

`<init>` is a constructor, and instance field initializers and instance initializer blocks get compiled into it too. So an exception from a field initializer shows `<init>` with the field's line. The angle brackets are there because the JVM needs method names that can't collide with anything you can write in Java.

```log
java.lang.ExceptionInInitializerError
	at RuleLab.main(RuleLab.java:98)
Caused by: java.lang.NumberFormatException: For input string: "x12"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at java.base/java.lang.Integer.parseInt(Integer.java:778)
	at RuleLab$Defaults.<clinit>(RuleLab.java:36)
	... 1 more

```

`<clinit>` is the static initializer: static field initializers and `static {}` blocks. The outer `ExceptionInInitializerError` only has the frame that *triggered* the class load, `main` at line 98\. The frame you want, `<clinit>` at line 36, is down in the `Caused by`. The elision rule from [the Caused by article](https://ggorantala.dev/java-caused-by-root-cause/?ref=ggorantala.dev) applies unchanged: that `... 1 more` is the `main` frame, shared with the outer trace.

### The proxy

```log
java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)
	at RuleLab$QtyRule.check(RuleLab.java:23)
	at RuleLab.lambda$viaProxy$1(RuleLab.java:78)               (1)
	at $Proxy0.check(Unknown Source)                            (2)
	at RuleLab.viaProxy(RuleLab.java:84)
	at RuleLab.main(RuleLab.java:96)

```

(2) is a class the JDK generated at run time. It implements `Rule`, and each method forwards to your `InvocationHandler`. (1) is the handler, which in my lab happens to be a lambda, so you get a `lambda$` name numbered `$1` because `viaLambda` already took `$0`. With `-XX:+ShowHiddenFrames` the lambda's generated class shows up too:

```log
	at RuleLab.lambda$viaProxy$1(RuleLab.java:78)
	at RuleLab$$Lambda/0x00007f4a5c000400.invoke(Unknown Source)
	at $Proxy0.check(Unknown Source)

```

The lambda trace from the same flag has `RuleLab$$Lambda/0x00007f0b74001800.accept(Unknown Source)` between your lambda and `forEach`. Different hex on every run, so don't match on it.

`P.java` shows the two other faces of a proxy. A proxy for `Runnable` prints like this:

```log
java.lang.IllegalStateException: from handler
	at P.lambda$main$0(P.java:5)
	at jdk.proxy1/jdk.proxy1.$Proxy0.run(Unknown Source)
	at P.main(P.java:6)

```

And when the handler throws a *checked* exception the interface doesn't declare, the proxy wraps it:

```log
java.lang.reflect.UndeclaredThrowableException
	at jdk.proxy1/jdk.proxy1.$Proxy1.go(Unknown Source)
	at P.main(P.java:9)
Caused by: java.io.IOException: checked
	at P.lambda$main$1(P.java:8)
	... 2 more

```

Root cause is the `Caused by`, same as always. If you've ever stared at an `InvocationTargetException` with a null message, [that one is the reflection cousin](https://ggorantala.dev/java-invocationtargetexception-null-message-real-cause/?ref=ggorantala.dev).

## Who names these frames: javac, the JVM, or Proxy

This took me well over a day of rebuilding the lab and re-reading Javadoc before it sorted itself out, and what finally organised it was one question per name: *who* picked this? It's one of three, and they behave differently.

**javac picks** `lambda$...$N`, `Outer$1`, `Outer$1Local`, `Outer$Mode$1`, bridge methods and `access$NNN`. All of these are synthetic. They exist in the class file with the synthetic flag set, they show up in traces because the JVM doesn't treat synthetic methods specially, and their names aren't a stable interface. The `N` is a class-wide counter. I didn't read javac's source for the exact assignment order; what I can tell you is what I observed above (member order, inner lambda before outer). That's enough to say don't build anything on it.

Line numbers on generated methods were the slow part. A bridge method has no source of its own, so its line table points at the declaration, and `access$000` does the same. The lambda body is different: it *is* your code, so its lines are real.

**The JVM picks** `<init>` and `<clinit>`. These are fixed names from the class file format. Nothing to configure.

**The runtime picks** `$ProxyN` and, with the right flag, `RuleLab$$Lambda/0x...`. The `Proxy` Javadoc is blunt about this: "The unqualified name of a proxy class is unspecified. The space of class names that begin with the string `"$Proxy"` should be, however, reserved for proxy classes." Package and module are unspecified too. In my lab, a proxy for a non-public interface (my nested `Rule`, no `public`) landed in the interface's own package, so the frame prints as bare `$Proxy0`. A proxy for a public interface in an exported package landed in `jdk.proxy1`. The Javadoc describes exactly that split. It also means grepping logs for `jdk.proxy1` is a habit I wouldn't build.

Why `Unknown Source` on the proxy frame? I saved the generated classes with `-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true` and ran `javap -v` on them. Neither `SourceFile` nor `LineNumberTable` is there. It's not your build. There's nothing to fix. (If you want the full story on those two attributes, it's in the [Unknown Source article](https://ggorantala.dev/java-stack-trace-unknown-source/?ref=ggorantala.dev).)

The lambda classes are the part with a history. The generated class that implements `Consumer` is a *hidden class*, and [JEP 371](https://openjdk.org/jeps/371?ref=ggorantala.dev) (JDK 15) says it plainly: "Methods of hidden classes are not shown in stack traces by default." The way to see them is the diagnostic flag above. `StackWalker` has an equivalent switch, `SHOW_HIDDEN_FRAMES`, which the Javadoc describes as a superset of `SHOW_REFLECT_FRAMES`; I'll use it properly in the [StackWalker article](https://ggorantala.dev/java-stackwalker-api/?ref=ggorantala.dev). People wanted these frames gone long before 15: JDK-8016334, filed in 2013 and closed as a duplicate, complains about a `StackTraceTest$$Lambda$1.run(Unknown Source)` frame making traces longer. I haven't run anything before 21, so I can't tell you which release in between started hiding them. Treat my version claims as "21 does this, JEP 371 says 15".

## The frame everyone blames

Four misreadings I've watched people (me included) make.

*"The doubled frame is recursion."* Same class, same method name, two consecutive frames, different line numbers. One of those lines is the class declaration or the `new Foo() {` line. That's a bridge. Real recursion repeats the same line, over and over.

*"The `access$000` frame is where it broke."* It's a trampoline. Look at the frame *above* it for what actually threw, and the frame *below* for who called.

*"`Unknown Source` on `$Proxy0` means someone built without `-g`."* No. Proxies never had the attributes.

*"I'll alert on `lambda$main$4`."* Don't. Add one lambda above it in the class and it's `lambda$main$5`. Your error grouping splits in two the day you deploy an unrelated change. It took me three rebuilds of the lab to believe the counter was class-wide.

And the hub's rule still holds: the top frame is where the `Throwable` was constructed, which for a lambda is often inside the JDK or deeper, not the lambda.

## What to change, and what it won't fix

Name the thing. If a lambda body is more than a line, pull it into a named method and call that. The trace then has a real name and a stable one:

```java
orders.forEach(Order::validate);   // no synthetic frame at all, see the mref trace above
orders.forEach(this::checkOne);    // frame is RuleLab.checkOne, not lambda$viaLambda$0

```

The cost is zero at runtime and one extra method in the class. The [lambdas and streams article](https://ggorantala.dev/java-stack-trace-lambdas-streams/?ref=ggorantala.dev) goes further, with a stream pipeline where this matters a lot more.

For proxies, use a named `InvocationHandler` class instead of a lambda, so the handler shows up as a frame with a real name. I'd expect that to read better, but I didn't run it this time, so check it against your own trace.

What *doesn't* help: running production with `-XX:+ShowHiddenFrames`. It's a diagnostic flag, it makes every lambda trace longer, and the `0x00007f...` addresses change per run, so you break whatever grouping you had. Use it on your laptop while you learn what the frames mean.

Compile with debug info on (see the Unknown Source article). Not for `$Proxy0`, but because the other frames in these traces need line numbers.

## Keeping it from biting again

When you group errors in a log pipeline, group on exception type plus the first frame in your own package that *isn't* synthetic. Strip `lambda$...$N` numbers and `$N` suffixes from the fingerprint, or keep the method name and drop the counter.

Version honesty, since a trace from a different JDK will look different from mine. Everything above is from OpenJDK 21.0.12.1\. `access$000` appears only when classes are compiled for a target older than 11; I produced it with `--release 8` on JDK 21 and didn't run a JDK 8 runtime. Hidden lambda frames are off by default as of JEP 371 (JDK 15). Proxy package and module names are unspecified, so they can differ between JDKs. I did not run JDK 17 or 25, and I did not run CGLIB or ByteBuddy proxies at all. Framework-generated classes with `$$Enhancer...` style names follow the same logic, but check your own trace; the [framework-noise article](https://ggorantala.dev/java-stack-trace-framework-noise/?ref=ggorantala.dev) is where I'll cover those.

## What to remember

- `lambda$method$N` is your lambda body. Read it. `N` is a per-class counter and changes when you add a lambda above it.
- A method reference leaves no synthetic frame. A lambda leaves one.
- The same method twice in a row with different lines is a bridge, not recursion.
- `access$000` means a pre-11 target and a private access across nested classes. Skip it, and ignore its line number.
- `<init>` covers constructors and field initializers; `<clinit>` is static init, and its frame sits in the `Caused by` of an `ExceptionInInitializerError`.
- `$Proxy0` with `Unknown Source` is normal, and its package name is unspecified.

## Things people paste into search when they hit these

**What does `lambda$main$0` mean in a Java stack trace?**  
The method javac generated from a lambda body in `main`. The `0` is a per-class counter, so it changes when you add or move a lambda. The line number inside it is real. Observed on JDK 21.

**What is `access$000` in a Java stack trace?**  
A synthetic accessor javac generates so an inner class can call a private member of the outer class. You see it when classes target something older than Java 11; since JEP 181 nested classes are nestmates and the frame disappears. Its line is the outer class declaration.

**Why does the same method appear twice in my stack trace?**  
If the two lines differ and one is a class declaration or an anonymous-class line, the second frame is a bridge method from generics. If the same line repeats many times, that's actual recursion, and you're heading toward a `StackOverflowError`.

**What do `<init>` and `<clinit>` mean in a stack trace?**  
`<init>` is a constructor (plus instance field initializers and initializer blocks). `<clinit>` is the class's static initializer. A failure in `<clinit>` is wrapped in `ExceptionInInitializerError`, and the real frames are in its `Caused by`.