redis/jedis · error · IllegalStateException

DISCARD without MULTI

Error message

DISCARD without MULTI

What it means

discard() cancels a buffered transaction and requires that multi() was called (inMulti == true), matching Redis semantics. If MULTI was never entered, there is no transaction to discard and the method throws IllegalStateException("DISCARD without MULTI").

Solutions

  1. Only call discard() after multi() was invoked; use close() for unconditional cleanup
  2. Guard with an inMulti/state check or catch IllegalStateException
  3. Ensure single ownership of the transaction lifecycle so close() isn't called after an explicit discard()

Example fix

// before
Transaction t = client.transaction();
t.discard(); // throws
// after
Transaction t = client.transaction();
t.multi();
t.set("k", "v");
t.discard();
Defensive patterns

Strategy: try-catch

Validate before calling

if (t.isInMulti()) t.discard(); // otherwise just close()

Type guard

boolean canDiscard(MultiDbTransaction t) { return t.isInMulti(); }

Try / catch

try { t.discard(); } catch (IllegalStateException e) { /* transaction was never in MULTI; safe to ignore or close */ }

Prevention

When it happens

Trigger: Calling discard() on a MultiDbTransaction before multi(), or after a prior discard()/exec() already cleared the state (close() also calls discard internally).

Common situations: Double cleanup in finally blocks (close() after explicit discard()); defensive discard() on a freshly created transaction; error-handling paths that discard unconditionally.

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/6bc5a7b07e8bc314. Report an issue: GitHub.

Appendix: source

Thrown at src/main/java/redis/clients/jedis/mcf/MultiDbTransaction.java:219

          formatted.add(response.get());
        } catch (JedisDataException e) {
          formatted.add(e);
        }
      }
      return formatted;

    } finally {
      inMulti = false;
      inWatch = false;
      commands.clear();
      releaseConnection(serverInMultiMode);
    }
  }

  @Override
  public final String discard() {
    if (!inMulti) {
      throw new IllegalStateException("DISCARD without MULTI");
    }

    try {
      // MULTI itself is only issued from exec(), so the server never started a transaction.
      // Buffered commands only exist locally, so there is nothing to roll back server-side
      // unless we have already acquired a connection for pre-MULTI traffic (e.g. WATCH).
      if (inWatch) {
        acquireConnection().sendCommand(UNWATCH);
        return connection.getStatusCodeReply();
      }
      return OK_STR;
    } finally {
      inMulti = false;
      inWatch = false;
      commands.clear();
      releaseConnection(false);
    }
  }

View on GitHub (pinned to 6dac31d4c2)