{"record":{"id":"ca045966e081c84d","repo":"oracle/graal","slug":"you-cannot-serialize-a-continuation-whilst-it-s-ru","errorCode":null,"errorMessage":"You cannot serialize a continuation whilst it's running, as this would have unclear semantics. Please suspend first.","messagePattern":"You cannot serialize a continuation whilst it's running, as this would have unclear semantics\\. Please suspend first\\.","errorType":"exception","errorClass":"IllegalContinuationStateException","httpStatus":null,"severity":"error","filePath":"espresso/src/org.graalvm.continuations/src/org/graalvm/continuations/ContinuationImpl.java","lineNumber":602,"sourceCode":"    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {\n        /*\n         * We read the context classloader here because we need the classloader that holds the\n         * user's app. If we use Class.forName() in this code we get the platform classloader\n         * because this class is provided by the VM, and thus can't look up methods of user classes.\n         * If we use the classloader of the entrypoint it breaks for Generator and any other classes\n         * we might want to ship with the VM that use this API. So we need the user's app class\n         * loader. We could walk the stack to find it just like ObjectInputStream does, but we go\n         * with the context classloader here to make it easier for the user to control.\n         */\n        state = State.INCOMPLETE;\n        readObjectExternalImpl(in, Thread.currentThread().getContextClassLoader());\n    }\n\n    @Override\n    synchronized void writeObjectExternal(ObjectOutput out) throws IOException {\n        State currentState = lock();\n        if (currentState == State.RUNNING) {\n            throw new IllegalContinuationStateException(\"You cannot serialize a continuation whilst it's running, as this would have unclear semantics. Please suspend first.\");\n        }\n        try {\n            ensureMaterialized();\n            // We start by writing out a header byte. The high nibble contains a major version. Old\n            // libraries will refuse to deserialize continuations with a higher version than what\n            // they recognize. New libraries may choose to continue supporting the old formats. The\n            // low nibble contains flags.\n            int header = FORMAT_VERSION << FORMAT_SHIFT;\n\n            out.writeByte(header);\n\n            out.writeObject(currentState);\n            out.writeObject(entryPoint);\n\n            if (currentState == State.SUSPENDED) {\n                FrameRecordSerializer.forOut(FORMAT_VERSION, out).writeRecord(stackFrameHead);\n            }\n        } finally {","sourceCodeStart":584,"sourceCodeEnd":620,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/espresso/src/org.graalvm.continuations/src/org/graalvm/continuations/ContinuationImpl.java#L584-L620","documentation":"IllegalContinuationStateException from ContinuationImpl.writeObjectExternal: serializing a continuation while its state is RUNNING is forbidden because the semantics (which frame state to persist?) are undefined. The continuation must be in a suspended (or new/completed) state before writeObject-style serialization.","triggerScenarios":"Calling ObjectOutputStream.writeObject on a continuation that is currently executing (resume() not yet returned); checkpointing from a monitor thread while the workload runs; serializing inside the entry point before suspending.","commonSituations":"Periodic checkpointers that snapshot state on a timer regardless of suspension; trying to migrate live tasks between nodes.","solutions":["Suspend the continuation first (SuspendCapability.suspend) and serialize only after resume() returned.","Trigger checkpoints from a suspension callback rather than an external timer thread.","Check getState() == SUSPENDED before serializing.","For live migration, design an explicit suspend-then-migrate handshake."],"exampleFix":"// before\n// checkpoint thread, while workload runs:\nout.writeObject(continuation); // RUNNING -> exception\n\n// after\nif (continuation.getState() == Continuation.State.SUSPENDED) {\n    out.writeObject(continuation);\n} else {\n    requestSuspend(); // workload suspends at next safe point; serialize after resume() returns\n}","handlingStrategy":"validation","validationCode":"if (continuation.getState() == Continuation.State.SUSPENDED) {\n    out.writeObject(continuation);\n} else {\n    requestSuspendThenCheckpoint(); // suspend first, serialize after resume() returns\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Checkpoint only from suspension points, not from timer threads.","Always check getState() == SUSPENDED before serializing.","Design migration as an explicit suspend-then-serialize handshake."],"tags":["continuations","illegal-state","serialization","checkpointing","graalvm","truffle"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}