oracle/graal · error · IllegalContinuationStateException

Suspend capabilities can only be used inside a running conti

Error message

Suspend capabilities can only be used inside a running continuation.

What it means

IllegalContinuationStateException from trySuspend when the calling thread owns the continuation but its state is not RUNNING, so the RUNNING->SUSPENDED CAS fails. The continuation already moved (e.g. suspended by another path, completed, or is mid-transition), so a second suspend is illegal.

Source

Thrown at espresso/src/org.graalvm.continuations/src/org/graalvm/continuations/ContinuationImpl.java:476

                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;
        }
        // set in #resume()
        assert state == State.RUNNING : state;
    }

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Call suspend() at most once per activation; after resume() returns from a suspend, do not suspend again until re-resumed.
  2. Structure the entry point so only one place performs the suspension decision.
  3. On this exception, check getState() and unwind instead of retrying.
  4. Keep suspend logic on the continuation thread only (also avoids error 595).

Example fix

// before
try {
    capability.suspend();
} catch (IllegalContinuationStateException e) {
    capability.suspend(); // double-suspend -> this error again
}

// after
if (continuation.getState() == Continuation.State.RUNNING) {
    capability.suspend();
} else {
    return; // already suspended/completed; unwind
Defensive patterns

Strategy: validation

Validate before calling

if (continuation.getState() == Continuation.State.RUNNING) {
    capability.suspend();
} else {
    // already suspended/completed; return instead of suspending again
}

Prevention

When it happens

Trigger: Calling SuspendCapability.suspend() twice; suspending from a re-entrant callback after the state already changed to SUSPENDED; suspend racing with a state transition driven by serialization or failure handling.

Common situations: Retrying suspend in a catch block after the first attempt partially progressed; multiple suspension points triggered by the same event; nested callbacks each attempting to suspend.

Related errors


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