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

  1. Call multi() before exec() when you intend an atomic MULTI/EXEC
  2. Track transaction state in your code so exec() is called at most once per multi()
  3. 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

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


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)