oracle/graal · error · IllegalContinuationStateException
Suspend capabilities can only be used inside a continuation.
Error message
Suspend capabilities can only be used inside a continuation.
What it means
IllegalContinuationStateException from ContinuationImpl.trySuspend (backing SuspendCapability.suspend()) when the calling thread is not the continuation's exclusive owner. Suspend capabilities are only usable from code physically executing inside the continuation on its resume thread; calling suspend() from an unrelated thread is rejected.
Source
Thrown at espresso/src/org.graalvm.continuations/src/org/graalvm/continuations/ContinuationImpl.java:473
sb.append(" Primitives: ");
for (int j = 1; j < record.primitives.length; j++) {
sb.append(record.primitives[j]);
if (j < record.pointers.length - 1) {
sb.append(", ");
}
}
sb.append("\n");
}
return sb.toString();
}
/**
* Called from {@link SuspendCapability#suspend()}. Suspends this continuation. If successful,
* execution resumes back at {@link #resume()}.
*/
void trySuspend() {
if (exclusiveOwner != Thread.currentThread()) {
throw new IllegalContinuationStateException("Suspend capabilities can only be used inside a continuation.");
}
if (!updateState(State.RUNNING, State.SUSPENDED)) {
throw new IllegalContinuationStateException("Suspend capabilities can only be used inside a running continuation.");
}
try {
suspend();
} catch (IllegalContinuationStateException e) {
if (!updateState(State.SUSPENDED, State.RUNNING)) {
// force failed state and maybe assert
State badState = forceState(State.RUNNING);
if (ASSERTIONS_ENABLED) {
AssertionError assertionError = new AssertionError(badState.toString());
assertionError.addSuppressed(e);
throw assertionError;
}
}
throw e;
}View on GitHub (pinned to a66e9ccd1d)
Solutions
- Only call SuspendCapability.suspend() from within the continuation entry point, on the thread that resumed it.
- Do not leak the capability to other threads; treat it as thread-confined.
- For cross-thread signaling, use a blocking queue/latch inside the continuation so the continuation's own thread performs the suspend.
- Ensure async callbacks re-enter the continuation thread before suspending.
Example fix
// before CompletableFuture.supplyAsync(work, foreignPool).thenRun(() -> capability.suspend()); // wrong thread // after // inside the continuation entry point, on its own thread: SomeResult r = blockingCall(); // blocks this thread instead capability.suspend(); // called by the continuation's own thread
Defensive patterns
Strategy: validation
Validate before calling
// inside the continuation entry point only:
if (Thread.currentThread() != resumeThread) {
throw new IllegalStateException("suspend must run on the continuation thread");
} Prevention
- Treat SuspendCapability as thread-confined; never share it.
- Use blocking primitives inside the continuation instead of foreign-thread callbacks.
- Ensure async callbacks hop back to the continuation thread before suspending.
When it happens
Trigger: Invoking capability.suspend() from outside the continuation body (e.g. application main thread, another worker); storing the SuspendCapability in a shared field and calling it from a scheduler thread; unit tests calling suspend() directly on a capability handed out earlier.
Common situations: Passing the capability to async framework callbacks (CompletableFuture completions on foreign thread pools) that call suspend() when they fire; sharing the capability across an executor.
Related errors
- You can't resume an already executing continuation.
- You can't resume a continuation while it is being serialized
- Only new or suspended continuation can be resumed
- This continuation has already completed successfully.
- This continuation has failed and must be discarded.
AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14).
Data as JSON: /api/errors/d4137eadbf299dea.
Report an issue: GitHub.