redis/jedis · error · IllegalStateException
EXEC without MULTI
Error message
EXEC without MULTI
What it means
exec() on MultiDbTransaction replays buffered commands inside a real server-side MULTI/EXEC. It requires that multi() was previously called on this object (inMulti == true); otherwise the transaction never entered MULTI state and there are no semantics for EXEC, so it throws IllegalStateException("EXEC without MULTI").
Solutions
- Call multi() before exec() when you intend an atomic MULTI/EXEC
- Track transaction state in your code so exec() is called at most once per multi()
- If commands were queued without multi(), they execute one-by-one; use status()/response() results instead of exec()
Example fix
// before
Transaction t = client.transaction();
t.set("k", "v");
t.exec(); // throws: no multi()
// after
Transaction t = client.transaction();
t.multi();
t.set("k", "v");
t.exec(); Defensive patterns
Strategy: type-guard
Validate before calling
if (!t.isInMulti()) t.multi(); // or throw before exec
Type guard
boolean canExec(MultiDbTransaction t) { return t.isInMulti(); } Try / catch
try { List<Object> res = t.exec(); } catch (IllegalStateException e) { /* ensure multi() was called or use queued-command results */ } Prevention
- Enforce multi() before exec() in transaction helper classes
- Call exec() exactly once per multi(); track consumed state
- Prefer try-with-resources with close() for cleanup instead of ad-hoc exec/discard
When it happens
Trigger: Calling exec() on a MultiDbTransaction that never had multi() invoked, or calling exec() a second time after a previous multi()/exec() cleared the state.
Common situations: Code that used the queued-commands API (set/get without multi) and then calls exec(); double-exec after error handling; reusing a closed/reset transaction object.
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
- WATCH inside MULTI is not allowed
- DISCARD without MULTI
- HashImport ' ' has been discarded
- Cannot use Jedis when in Multi. Please use Transaction or…
- Cannot use Jedis when in Pipeline. Please use Pipeline or…
AI-assisted analysis of redis/jedis@6dac31d4c2 (2026-09-08).
Data as JSON: /api/errors/1a41bc99330bec91.
Report an issue: GitHub.
Appendix: source
Thrown at src/main/java/redis/clients/jedis/mcf/MultiDbTransaction.java:142
}
@Override
public void close() {
try {
if (inMulti) {
discard();
} else if (inWatch) {
unwatch();
}
} finally {
releaseConnection(false);
}
}
@Override
public final List<Object> exec() {
if (!inMulti) {
throw new IllegalStateException("EXEC without MULTI");
}
boolean serverInMultiMode = false;
try {
Connection conn = acquireConnection();
Object multiReply = conn
.executeCommand(new CommandObject<>(new CommandArguments(MULTI), NO_OP_BUILDER));
if (!bytesEquals(OK_IN_BYTES, multiReply)) {
Object response = multiReply instanceof byte[] ? SafeEncoder.encode((byte[]) multiReply)
: multiReply;
throw new JedisDataException("Unexpected response: " + response);
}
serverInMultiMode = true;
commands.forEach((command) -> conn.sendCommand(command.getKey()));
// following connection.getMany(int) flushes anyway, so no flush here.View on GitHub (pinned to 6dac31d4c2)