oracle/graal · error · IllegalContinuationStateException

Only new or suspended continuation can be resumed

Error message

Only new or suspended continuation can be resumed

What it means

Catch-all IllegalContinuationStateException from resume() when the observed state is none of RUNNING, COMPLETED, FAILED, or LOCKED. Because the state field is read racily, the exact state can change between the CAS attempts and the report; this generic message covers transient states such as NEW-with-concurrent-transition or mid-serialization INCOMPLETE.

Source

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

            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);
        }
    }

    @Override
    @SuppressWarnings("deprecation")
    public String toDebugString() {

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Externalize a lock so only one thread performs resume() at a time; the internal state machine is not a substitute for caller synchronization.
  2. Re-check getState() after the exception and decide based on the fresh value.
  3. Treat this exception as a concurrency bug in caller code: audit all resume/suspend/serialize call sites for races.
  4. Single-thread continuation ownership (actor/pinned worker) removes the class of problem.

Example fix

// before
executors.forEach(ex -> ex.submit(() -> continuation.resume())); // racing resumes

// after
// single owner:
workerThread.submit(() -> {
    while (!continuation.resume()) { /* schedule */ }
});
Defensive patterns

Strategy: try-catch

Try / catch

try {
    continuation.resume();
} catch (IllegalContinuationStateException e) {
    switch (continuation.getState()) {
        case SUSPENDED, NEW -> continuation.resume(); // safe to retry once
        default -> throw e; // real lifecycle problem
    }
}

Prevention

When it happens

Trigger: Concurrent resume() calls from multiple threads on the same fresh continuation where the loser of the first CAS reads a stale state; resume() racing with serialization state transitions (INCOMPLETE/LOCKED).

Common situations: Multi-threaded schedulers sharing one continuation without external synchronization; tests hammering resume from several threads to probe races.

Related errors


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