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

  1. Call transaction.multi() before exec() when using manual mode (doMulti=false).
  2. Prefer transaction(true) or transaction() so MULTI is issued automatically.
  3. Track whether the transaction was already exec'd; create a new Transaction for a new block.
  4. 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

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


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)