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
- Externalize a lock so only one thread performs resume() at a time; the internal state machine is not a substitute for caller synchronization.
- Re-check getState() after the exception and decide based on the fresh value.
- Treat this exception as a concurrency bug in caller code: audit all resume/suspend/serialize call sites for races.
- 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
- Serialize all resume() calls through one thread or lock.
- Treat this message as a caller-side race signal; audit concurrency.
- Pin continuation ownership to one worker (actor style).
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
- You can't resume an already executing continuation.
- You can't resume a continuation while it is being serialized
- Suspend capabilities can only be used inside a continuation.
- 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/a530710d97f8270b.
Report an issue: GitHub.