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
- Ensure client.close()/destroy() is not called until all in-flight multi.exec() promises have resolved (await them before shutdown).
- Guard multi.exec() with if (client.isOpen) and skip or recreate the transaction when the client is closed.
- If you need the transaction to survive reconnects, re-issue WATCH then MULTI/EXEC after reconnecting via await client.connect().
- 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
- Never call client.close()/destroy() while a multi.exec() is pending; await it first.
- Check client.isOpen before building a MULTI if the client lifecycle is uncertain.
- Centralize client teardown so shutdown cannot race in-flight transactions.
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
- Client reconnected after WATCH
- Cluster already open
- One (or more) of the watched keys has been changed
- Socket already opened
- The client is closed
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)