{"record":{"id":"1a41bc99330bec91","repo":"redis/jedis","slug":"exec-without-multi-1a41bc","errorCode":null,"errorMessage":"EXEC without MULTI","messagePattern":"EXEC without MULTI","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"src/main/java/redis/clients/jedis/mcf/MultiDbTransaction.java","lineNumber":142,"sourceCode":"  }\n\n  @Override\n  public void close() {\n    try {\n      if (inMulti) {\n        discard();\n      } else if (inWatch) {\n        unwatch();\n      }\n    } finally {\n      releaseConnection(false);\n    }\n  }\n\n  @Override\n  public final List<Object> exec() {\n    if (!inMulti) {\n      throw new IllegalStateException(\"EXEC without MULTI\");\n    }\n\n    boolean serverInMultiMode = false;\n    try {\n      Connection conn = acquireConnection();\n\n      Object multiReply = conn\n          .executeCommand(new CommandObject<>(new CommandArguments(MULTI), NO_OP_BUILDER));\n      if (!bytesEquals(OK_IN_BYTES, multiReply)) {\n        Object response = multiReply instanceof byte[] ? SafeEncoder.encode((byte[]) multiReply)\n            : multiReply;\n        throw new JedisDataException(\"Unexpected response: \" + response);\n      }\n\n      serverInMultiMode = true;\n\n      commands.forEach((command) -> conn.sendCommand(command.getKey()));\n      // following connection.getMany(int) flushes anyway, so no flush here.","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/redis/jedis/blob/6dac31d4c224fb3257c216f3985340c6f500cdcb/src/main/java/redis/clients/jedis/mcf/MultiDbTransaction.java#L124-L160","documentation":"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\").","triggerScenarios":"Calling exec() on a MultiDbTransaction that never had multi() invoked, or calling exec() a second time after a previous multi()/exec() cleared the state.","commonSituations":"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.","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()"],"exampleFix":"// before\nTransaction t = client.transaction();\nt.set(\"k\", \"v\");\nt.exec(); // throws: no multi()\n// after\nTransaction t = client.transaction();\nt.multi();\nt.set(\"k\", \"v\");\nt.exec();","handlingStrategy":"type-guard","validationCode":"if (!t.isInMulti()) t.multi(); // or throw before exec","typeGuard":"boolean canExec(MultiDbTransaction t) { return t.isInMulti(); }","tryCatchPattern":"try { List<Object> res = t.exec(); } catch (IllegalStateException e) { /* ensure multi() was called or use queued-command results */ }","preventionTips":["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"],"tags":["redis","transactions","exec","illegal-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"6dac31d4c224fb3257c216f3985340c6f500cdcb","analyzedAt":"2026-09-08T04:55:01.204Z","contentChangedAt":"2026-09-08T04:55:01.204Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}