oracle/graal · error · IllegalContinuationStateException

You can't resume an already executing continuation.

Error message

You can't resume an already executing continuation.

What it means

IllegalContinuationStateException raised by resume() when the continuation's state is RUNNING. A continuation that is currently executing on a thread cannot be re-entered; resume is only legal from the NEW or SUSPENDED states. This typically means resume() was called re-entrantly or concurrently.

Source

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

            try {
                return start0();
            } finally {
                clearExclusiveOwner();
            }
        } else if (updateState(State.SUSPENDED, State.RUNNING)) {
            setExclusiveOwner();
            try {
                // We are ready to resume, make sure the VM has the most up-to-date frames
                ensureDematerialized();
                assert stackFrameHead == null;
                return resume0();
            } finally {
                clearExclusiveOwner();
            }
        } else {
            // illegal state for resume: neither suspended nor new
            switch (state) {
                case RUNNING -> throw new IllegalContinuationStateException("You can't resume an already executing continuation.");
                case COMPLETED ->
                    throw new IllegalContinuationStateException("This continuation has already completed successfully.");
                case FAILED -> throw new IllegalContinuationStateException("This continuation has failed and must be discarded.");
                case LOCKED -> throw new IllegalContinuationStateException("You can't resume a continuation while it is being serialized or deserialized.");
                // this is racy so ensure we have a general error message in those case
                default -> throw new IllegalContinuationStateException("Only new or suspended continuation can be resumed");
            }
        }
    }

    @Override
    public synchronized StackTraceElement[] getRecordedFrames() {
        State currentState = lock();
        try {
            ensureDematerialized();
            return getRecordedFrames0();
        } finally {
            unlock(currentState);

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Only call resume() after the previous resume() returned (i.e. after suspend or completion).
  2. Serialize access to the continuation with a per-continuation lock or single-owner thread.
  3. If self-resume is intended, restructure so the entry point suspends instead of re-resuming.
  4. Check getState() == SUSPENDED before resume as a cheap pre-condition (still race-prone; keep the try-catch).

Example fix

// before
// thread A and thread B both do:
continuation.resume(); // one hits 'already executing'

// after
synchronized (continuation) {
    if (continuation.getState() == Continuation.State.SUSPENDED) {
        continuation.resume();
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if (continuation.getState() != Continuation.State.SUSPENDED && continuation.getState() != Continuation.State.NEW) {
    // do not resume now
}

Try / catch

try {
    continuation.resume();
} catch (IllegalContinuationStateException e) {
    // another thread is running it; back off and reschedule
}

Prevention

When it happens

Trigger: Calling resume() from inside the continuation's own entry point (self-resume); a second thread calling resume() while the first is still inside resume(); resume() invoked from a listener/callback fired during continuation execution.

Common situations: Wrapping resume() in a scheduler that retries on failure without waiting for completion; recursive task schedulers where a task triggers its own continuation; missing synchronization around a shared continuation object.

Related errors


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