skip to content

Inside a Redis Lua script you can invoke commands with either redis.call or redis.pcall. What is the difference in how they handle a command that fails, and which should you reach for?

level: seniorimportance: should knowfreq 30%

answer

  1. call = raise, abort script, error to client
  2. pcall = return table with .err, script continues
  3. Error table is truthy → silent wrong path
  4. No rollback either way — validate first, write last
  5. redis.error_reply / status_reply for client-facing codes

basics

~20 s

redis.call raises a Lua error on command failure, aborting the script and returning the error to the client. redis.pcall returns the error as a Lua table instead, so the script keeps running and must inspect it. Default to redis.call; use pcall only when you will handle the failure.

solid answer

~50 s

Both execute a Redis command inside the script. They differ in the failure path: - **`redis.call`** — a failing command (wrong type, syntax error, OOM refusal) raises a Lua error. Execution stops immediately and the error is returned to the calling client, wrapped as a script error. Writes already performed still stand — there is no rollback. - **`redis.pcall`** — the error is *caught* and returned as a Lua table with an `err` field. The script continues, and it is your responsibility to test the result. Prefer `redis.call`: fail-fast is the correct default, and an uninspected `pcall` result silently becomes a success path, so a `WRONGTYPE` gets treated as a value and the script writes garbage. Use `redis.pcall` only where the failure is expected and you have a plan — e.g. attempting an operation that may legitimately fail and taking a different branch. You can also shape client-visible outcomes with `redis.error_reply('MSG')` and `redis.status_reply('OK')`.

code

lua · 7 lines
lua
-- KEYS[1] happens to hold a LIST, not a string
local n = redis.pcall('INCR', KEYS[1])   -- returns {err = 'WRONGTYPE ...'}
if n then                                 -- a table is truthy: this passes!
  redis.call('SET', KEYS[2], tostring(n)) -- writes garbage, no error surfaces
end

-- with redis.call the script would abort here and the client would see WRONGTYPE

go deeper

for a junior

State the mechanical difference: call raises and stops the script, pcall returns an error value and continues.

for a middle

Add that the pcall result is a table with an err field, that it is truthy so it must be checked explicitly, and that call is the sensible default.

for a senior

Connect it to no-rollback: order scripts so writes come last, use pcall only for expected failures, and surface stable domain codes with redis.error_reply.

for a principal

Treat the script's error surface as an API contract — which failures are business outcomes, which are infrastructure, and how callers are expected to branch and retry.

## Two ways to run a command A Redis Lua script talks to the data through `redis.call(cmd, arg…)` or `redis.pcall(cmd, arg…)`. On success they behave identically: the command executes inline, and its reply is converted to a Lua value (integer reply → number, bulk string → string, nil reply → `false`, array → table, status reply → table with an `ok` field). The difference is what happens when the command **fails** — a wrong-type operation on an existing key, a malformed argument, a write refused because `maxmemory` is exceeded, a command not permitted in the current context. **`redis.call` propagates the failure as a Lua error.** The Lua error unwinds immediately; the script does not continue; Redis returns the error to the client that issued `EVAL`/`EVALSHA`/`FCALL`, typically preserving the original error code (`WRONGTYPE`, `OOM`, …) so client code can branch on it. **`redis.pcall` catches it.** Instead of raising, it returns a Lua table containing an `err` field with the message. The script continues running on the next line as if nothing had gone wrong. ```lua local res = redis.pcall('INCR', KEYS[1]) if type(res) == 'table' and res.err then -- handle it: fall back, return a custom error, log via redis.log end ``` ## Why fail-fast is the right default The dangerous property of `pcall` is that its error value is *truthy* in Lua. A table is not `nil` and not `false`, so `if res then` succeeds on failure. Code written for the happy path silently accepts the error object and carries on — comparing it numerically (yielding another error, or `nil` from `tonumber`), storing it, or treating a failed read as "key was empty" and overwriting real data. Combine that with Redis's lack of rollback and you get the worst outcome: a script that failed in the middle, kept going, and left a partially-updated, internally inconsistent state that nobody noticed because no error reached the client. So the rule is: **`redis.call` unless you are going to inspect the result**. If you type `pcall`, the very next lines should test for `res.err`. ## Where pcall genuinely helps - **Expected, recoverable failures.** You attempt an operation that may fail by design — for instance operating on a key that might hold the wrong type in a heterogeneous keyspace — and you want to take a different branch rather than abort. - **Cleanup or best-effort steps.** A trailing bookkeeping command whose failure should not fail the whole operation. - **Custom error surfaces.** Catch the raw error and return your own domain error, e.g. `return redis.error_reply('RATE_LIMITED')`, so clients branch on a stable code instead of parsing Redis internals. ## Shaping what the client sees Two helpers matter alongside these: - `redis.error_reply(msg)` returns a value that Redis converts into an error reply to the client. Convention is an uppercase code as the first word (`return redis.error_reply('OUT_OF_STOCK')`) so callers can match on it. Simply `return {err = 'OUT_OF_STOCK'}` is equivalent. - `redis.status_reply(msg)` produces a simple status reply such as `OK`. - `error('boom')` raises a plain Lua error, which reaches the client as a script error with location information — usable, but less clean than an explicit `error_reply`. `redis.log(redis.LOG_WARNING, msg)` writes to the server log, useful when a `pcall` branch swallows something you still want recorded — but remember the server log is not per-request context, and logging on a hot path costs time on the blocking thread. ## Interaction with the no-rollback reality Neither function changes the fundamental behaviour: commands already executed have taken effect and will be propagated to replicas and the AOF. `call` merely stops you from doing *more* damage after a failure; it does not undo what happened. The structural defence is to arrange scripts so that all validation and reads happen first and all writes happen at the end, so that an abort lands in the validation phase where nothing has been mutated. That ordering has a second benefit: a script that has not yet written is still killable with `SCRIPT KILL`. ## Interview-ready summary `call` = raise and abort; `pcall` = capture and continue. Default to `call` because an unchecked `pcall` result converts a failure into a silent wrong answer. Use `pcall` deliberately, check `res.err` immediately, and return a domain-specific `redis.error_reply` when you want callers to distinguish business outcomes from infrastructure errors.

  • A script does three writes and the second one fails under redis.call. What state is the data in?
    The first write stands, the second did not happen, the third never ran, and the client receives the error. Redis does not roll back script effects, so the key set is left half-updated. The defence is structural: do all reads and validation first and group the writes at the end, so an abort happens before anything has been mutated.
  • How do you make a client distinguish a business outcome from an infrastructure failure raised inside a script?
    Return redis.error_reply with a stable uppercase code such as 'OUT_OF_STOCK' (or the equivalent {err = '...'} table) for the business case, and let genuine command failures propagate through redis.call with their native codes like WRONGTYPE or OOM. The client then matches on your code and treats anything else as an infrastructure error worth retrying or alerting on.

saying these in an interview costs you the question

  • Treating redis.pcall as 'the safe one' and using it everywhere
  • Testing a pcall result with `if res then` — the error table is truthy
  • Believing an aborted redis.call undoes earlier writes in the script
  • Thinking pcall protects against the script blocking the server
  • Returning raw Lua error strings and expecting clients to parse them reliably

context