redis/node-redis · error · ClientClosedError

The client is closed

Error message

The client is closed

What it means

Thrown at the start of _executeMulti (the MULTI/EXEC transaction path) when the underlying socket is no longer open. The client was closed via close()/destroy(), or the connection was lost and not re-established, before multi.exec() could send the MULTI command. No transaction is sent to Redis.

Solutions

  1. Ensure client.close()/destroy() is not called until all in-flight multi.exec() promises have resolved (await them before shutdown).
  2. Guard multi.exec() with if (client.isOpen) and skip or recreate the transaction when the client is closed.
  3. If you need the transaction to survive reconnects, re-issue WATCH then MULTI/EXEC after reconnecting via await client.connect().
  4. Reconnect the client (await client.connect()) before retrying the transaction.

Example fix

// before
const multi = client.multi();
multi.set('k', 'v');
await client.close();
await multi.exec(); // throws ClientClosedError

// after
const multi = client.multi();
multi.set('k', 'v');
const res = await multi.exec();
await client.close();
Defensive patterns

Strategy: validation

Validate before calling

if (!client.isOpen) {
  throw new Error('Cannot execute MULTI: client is closed. Connect or reconnect first.');
}

Type guard

function canExecuteMulti(client: RedisClientType): boolean {
  return client.isOpen;
}

Try / catch

try {
  await multi.exec();
} catch (e) {
  if (e instanceof ClientClosedError) {
    // client closed before exec; re-connect and rebuild the transaction
  } else throw e;
}

Prevention

When it happens

Trigger: Calling const multi = client.multi(); ...then client.close() or client.destroy(); ...then await multi.exec(). Also fires when the socket dies mid-transaction-building and auto-reconnect is disabled or exhausted, so #socket.isOpen is false by the time exec() runs.

Common situations: Application shutdown logic that closes the client in a finally block while a pending multi.exec() is still awaited; reconnectStrategy set to false causing the client to stay closed after a network blip; calling exec() on a multi object created from a previous client instance that has since been torn down.

Related errors


AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11). Data as JSON: /api/errors/c6f46ec38a10eaab. Report an issue: GitHub.

Appendix: source

Thrown at packages/client/lib/client/index.ts:1928

  /**
   * @internal
   */
  async _executeMulti(
    commands: Array<RedisMultiQueuedCommand>,
    selectedDB?: number,
    slotNumber?: number,
    chainId = Symbol('MULTI Chain')
  ) {
    assertNoHimportSessionCommands(commands);

    const dirtyWatch = this._self.#dirtyWatch;
    this._self.#dirtyWatch = undefined;
    const watchEpoch = this._self.#watchEpoch;
    this._self.#watchEpoch = undefined;

    if (!this._self.#socket.isOpen) {
      throw new ClientClosedError();
    }

    if (dirtyWatch) {
      throw new WatchError(dirtyWatch);
    }

    if (watchEpoch && watchEpoch !== this._self.socketEpoch) {
      throw new WatchError('Client reconnected after WATCH');
    }

    const batchSize = commands.length;

    return trace(CHANNELS.TRACE_BATCH,
      async () => {
        const typeMapping = this._commandOptions?.typeMapping;
        const promises: Array<Promise<unknown>> = [
          this._self.#queue.addCommand(['MULTI'], { chainId, slotNumber }),
        ];

View on GitHub (pinned to 90fd0652bc)