oracle/graal · error · IOException

Can't serialize continuation with static frames from methods

Error message

Can't serialize continuation with static frames from methods of hidden classes: %s.%s

What it means

Thrown as IOException by FrameRecordSerializer.writeMethodHolder while serializing frames: the frame's method is declared in a hidden class (method.getDeclaringClass().isHidden()) and the serializer could not fall back to the receiver. Hidden classes (lambdas, MethodHandles.Lookup.defineHiddenClass, records in some compilers) have no stable lookup-by-name, and unlike instance frames — where the lambda's `this` in slot THIS_POS lets the reader recover the class — a static frame has no receiver, so the declaring class would be unrecoverable on deserialization.

Source

Thrown at espresso/src/org.graalvm.continuations/src/org/graalvm/continuations/FrameRecordSerializer.java:167

        } catch (NoSuchMethodException e) {
            throw new IOException(e);
        }
    }

    private void writeMethodHolder(Method method, Object receiver) throws IOException {
        assert out != null;
        if (receiver != null && method.getDeclaringClass() == receiver.getClass()) {
            /*
             * Some classes such as Lambda classes can't be looked up by name. This is a JVM
             * optimization designed to avoid contention on the global dictionary lock, but it means
             * we need another way to get the class for the method. Fortunately, lambdas always have
             * an instance, so we can read it out of the first pointer slot.
             */
            out.writeBoolean(true);
        } else {
            out.writeBoolean(false);
            if (method.getDeclaringClass().isHidden()) {
                throw new IOException("Can't serialize continuation with static frames from methods of hidden classes: %s.%s".formatted(method.getDeclaringClass().getName(), method.getName()));
            }
            writeString(method.getDeclaringClass().getName());
        }
    }

    private void writeFrame(ContinuationImpl.FrameRecord cursor) throws IOException {
        assert out != null;
        Method method = cursor.method;
        writeMethodHolder(method, cursor.pointers.length > THIS_POS ? cursor.pointers[THIS_POS] : null);
        writeString(method.getName());
        out.writeObject(cursor.pointers);
        out.writeObject(cursor.primitives);
        writeMethodTypes(method);
        out.writeInt(cursor.bci);
    }

    private void writeMethodTypes(Method method) throws IOException {
        assert out != null;

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Move the code that suspends out of the hidden/static frame into a normal named (non-hidden) class or instance method.
  2. Convert the static helper into an instance method so the receiver (`this`) is recorded in THIS_POS and the class is recoverable on read.
  3. Identify the hidden class on the stack at suspend time from the exception message (holder.name + method name) and restructure that call site.
  4. If a framework generates the hidden class, check for a config option to emit named classes instead (e.g. disable lambda metafactory hiding or use -Djava.lang.invoke.stringConcat / equivalent toggles for the generator).

Example fix

// before
static CompletableFuture<Object> f = supplyAsync(MyHidden::work); // static frame in hidden class
...
Continuation.suspend(); // inside work() -> writeMethodHolder throws on serialize

// after
class WorkTask implements Supplier<Object> { // named, instance method: receiver recorded
    public Object get() { Continuation.suspend(); return result; }
}
static CompletableFuture<Object> f = supplyAsync(new WorkTask());
Defensive patterns

Strategy: validation

Validate before calling

// before suspending, check the current stack for static frames of hidden classes
StackWalker.getInstance().forEach(f -> {
    Class<?> dc = f.getDeclaringClass();
    if (dc.isHidden() && (f.isStaticMethod() || f.getDeclaringClass().getName().contains("$$Lambda"))) {
        throw new IllegalStateException("Cannot suspend in hidden-class frame: " + f);
    }
});

Try / catch

try { bytes = Continuation.serialize(cont); } catch (IOException e) { /* message names the hidden class.method: move that code to a named class */ }

Prevention

When it happens

Trigger: Suspending (and thus serializing) inside a static method of a hidden class: a static lambda body (e.g. a static method reference invoked reflectively), MethodHandles hidden-class targets, or hidden classes generated by frameworks, where the frame has no `this` pointer.

Common situations: Framework-generated bytecode (mocking libs, AOP, ORM enhancers, JSP/GraalVM substrate-generated classes) running on the continuation stack when it suspends; method references to static helpers of hidden classes; Lombok/annotation-processor generated hidden classes on the stack at suspend time.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/9ac827f0962d9ca0. Report an issue: GitHub.