redis/jedis · error · IllegalStateException
EXEC without MULTI
Error message
EXEC without MULTI
What it means
exec() can only run while a transaction is open. ReliableTransaction tracks this with the inMulti flag and throws IllegalStateException('EXEC without MULTI') if exec() is called before multi() or after a previous exec()/discard() closed the transaction. This is a client-side state guard preventing a protocol desync with the server.
Solutions
- Call multi() before exec(), or obtain the transaction through an API that starts MULTI automatically.
- Create a new Transaction instance for each transaction instead of reusing a closed one.
- Guard exec() behind a check of the transaction status/inMulti flag in your code.
Example fix
// before Transaction t = jedis.multi(); t.exec(); t.exec(); // IllegalStateException // after Transaction t = jedis.multi(); List<Object> results = t.exec(); // open a new transaction for the next batch
Defensive patterns
Strategy: type-guard
Validate before calling
if (!transaction.isOpen()) { // or track your own multiOpen flag
throw new IllegalStateException("Call multi() before exec()");
} Type guard
boolean canExec(Transaction t) { return t != null && t.status().isOpen(); } Try / catch
try {
results = transaction.exec();
} catch (IllegalStateException e) {
// transaction already closed or never opened: create a new one
transaction = jedis.multi();
} Prevention
- Create a fresh Transaction per unit of work; never re-exec a closed transaction.
- Structure code as multi() -> queue -> exec()/discard() with no early returns skipping multi().
- Avoid calling exec() in finally blocks.
When it happens
Trigger: Calling exec() on a transaction that was never opened with multi(); calling exec() twice on the same ReliableTransaction; reusing a transaction object after discard() or after it was reset/cleared.
Common situations: Code that calls exec() in a finally block while the transaction creation path failed before multi(); loop code re-executing a finished transaction; manually constructed ReliableTransaction instances bypassing factory methods.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- DISCARD without MULTI
- HashImport ' ' has been discarded
- Cannot use Jedis when in Multi. Please use Transaction or…
- It is not allowed to create Transaction from this
- MULTI command failed. Received response:
AI-assisted analysis of redis/jedis@6dac31d4c2 (2026-09-08).
Data as JSON: /api/errors/3c84b549ae4b4f88.
Report an issue: GitHub.
Appendix: source
Thrown at src/main/java/redis/clients/jedis/ReliableTransaction.java:161
}
}
@Deprecated // TODO: private
public final void clear() {
if (broken) {
return;
}
if (inMulti) {
discard();
} else if (inWatch) {
unwatch();
}
}
@Override
public List<Object> exec() {
if (!inMulti) {
throw new IllegalStateException("EXEC without MULTI");
}
try {
// processPipelinedResponses(pipelinedResponses.size());
// do nothing
connection.sendCommand(EXEC);
List<Object> unformatted = connection.getObjectMultiBulkReply();
if (unformatted == null) {
pipelinedResponses.clear();
return null;
}
List<Object> formatted = new ArrayList<>(unformatted.size());
for (Object o : unformatted) {
try {
Response<?> response = pipelinedResponses.poll();
response.set(o);View on GitHub (pinned to 6dac31d4c2)