Modelled on src/multi.c, Redis 7.2.14, with the queueing path from processCommand in src/server.c.
A transaction gives you isolation, not rollback. Between EXEC and the last queued command no other client's command runs, but if one command fails partway through, the commands before it stay applied and the ones after it still run (multi.c:190–243). The two ways a transaction can fail are not the same thing, and the page exists to show the difference.
MULTI, EXEC, DISCARD, WATCH, UNWATCH, RESET, SET, GET, INCR, DECR, SADD, LPUSH, LPOP, RPOP, SPOP, PING.
A command that fails to queue — unknown command, or the wrong number of
arguments — is rejected before it is ever stored (server.c:3885), and the
rejection sets CLIENT_DIRTY_EXEC (multi.c:107). EXEC then discards
everything and replies:
-EXECABORT Transaction discarded because of previous errors.
server.c:1873, replied at multi.c:172
A command that queues cleanly and then fails at EXEC — INCR on a string, a wrong-type operation — does not abort anything. The loop at multi.c:190 has no break: that command's error becomes one element of the reply array and every other command still runs. Nothing is rolled back; multi.c:253 only frees the queue.
A watched key that changed is a third outcome and not an error at all. EXEC
replies a null array (multi.c:174) — (nil) in redis-cli.
Once the client is dirty, queueMultiCommand stores nothing further, though the client still gets +QUEUED (multi.c:67, server.c:4168).
A write is not the only thing that breaks a watch. Expiry (db.c:1687) and eviction (evict.c:679) both reach touchWatchedKey, and FLUSHDB, FLUSHALL and SWAPDB mark every watcher at once (db.c:619, db.c:1525). A watched key that merely expires between WATCH and EXEC is enough on its own — EXEC checks for it at multi.c:160.
A WATCH, if you use one, must be issued on this same connection before MULTI.