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

  1. Only call SuspendCapability.suspend() from within the continuation entry point, on the thread that resumed it.
  2. Do not leak the capability to other threads; treat it as thread-confined.
  3. For cross-thread signaling, use a blocking queue/latch inside the continuation so the continuation's own thread performs the suspend.
  4. 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

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


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