redis/jedis · error · IllegalStateException
EXEC without MULTI
Error message
EXEC without MULTI
What it means
Transaction.exec() must run inside a MULTI block. If WATCH/MONITOR-style usage or a manual-MULTI transaction never called multi(), the transaction is not active and Jedis throws IllegalStateException before sending EXEC.
Solutions
- Call transaction.multi() before exec() when using manual mode (doMulti=false).
- Prefer transaction(true) or transaction() so MULTI is issued automatically.
- Track whether the transaction was already exec'd; create a new Transaction for a new block.
- Use try/finally to ensure multi() is called on the path that reaches exec().
Example fix
// before
Transaction t = jedis.transaction(false);
t.watch("k");
List<Object> res = t.exec(); // throws: no multi()
// after
Transaction t = jedis.transaction(false);
t.watch("k");
t.multi();
List<Object> res = t.exec(); Defensive patterns
Strategy: validation
Validate before calling
if (!inMultiManually) tx.multi(); // ensure MULTI before exec
Try / catch
try {
List<Object> res = tx.exec();
} catch (IllegalStateException e) {
log.error("Transaction not in MULTI: {}", e.getMessage());
} Prevention
- Use transaction(true) to let Jedis issue MULTI automatically
- Never reuse a Transaction after exec()
- Ensure multi() is called in manual-MULTI flows before exec()
When it happens
Trigger: Creating a Transaction with transaction(false) (manual MULTI) and calling exec() without first calling multi(); calling exec() twice — the second call after the block already ended.
Common situations: Using WATCH + manual MULTI flows where an early return/exception skipped multi(); reusing a Transaction object after a prior exec; conditional logic that only sometimes enters MULTI.
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
- Please close pipeline or multi block before calling this…
- DISCARD without MULTI
- Cannot use Jedis when in Multi. Please use Transaction or…
- It is not allowed to create Transaction from this
- Range must not be null.
AI-assisted analysis of redis/jedis@6dac31d4c2 (2026-09-08).
Data as JSON: /api/errors/b3e7d91fea4cb673.
Report an issue: GitHub.
Appendix: source
Thrown at src/main/java/redis/clients/jedis/Transaction.java:169
}
}
@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 {
// ignore QUEUED (or ERROR)
// processPipelinedResponses(pipelinedResponses.size());
List<Object> queuedCmdResponses = connection.getMany(1 + pipelinedResponses.size());
connection.sendCommand(EXEC);
List<Object> unformatted;
try {
unformatted = connection.getObjectMultiBulkReply();
} catch (JedisDataException jce) {
// A command may fail to be queued, so there may be an error before EXEC is called
// In this case, the server will discard all commands in the transaction and return the EXECABORT error.
// Enhance the final error with suppressed errors.
queuedCmdResponses.stream()View on GitHub (pinned to 6dac31d4c2)