redis/node-redis · error · ClientClosedError

The client is closed

Error message

The client is closed

What it means

Thrown by RedisSocket.quit() when #isOpen is false. quit() sends QUIT to Redis for a graceful shutdown, which requires an active connection. Calling it on a client that is already closed (or was never connected) is a no-op that the library rejects rather than silently ignoring.

Solutions

  1. Guard with if (client.isOpen) await client.quit().
  2. Track a shutdown flag so quit()/close()/destroy() run exactly once.
  3. Prefer client.disconnect() for idempotent teardown if you do not need the graceful QUIT reply.
  4. Use a single shutdown coordinator rather than calling quit() from multiple handlers.

Example fix

// before
await client.quit();
// later, in another handler:
await client.quit(); // throws ClientClosedError

// after
let shuttingDown = false;
async function shutdown() {
  if (shuttingDown) return;
  shuttingDown = true;
  if (client.isOpen) await client.quit();
}
Defensive patterns

Strategy: validation

Validate before calling

if (client.isOpen) {
  await client.quit();
}

Type guard

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

Try / catch

try {
  await client.quit();
} catch (e) {
  if (e instanceof ClientClosedError) return; // already closed, fine
  throw e;
}

Prevention

When it happens

Trigger: Calling await client.quit() after already calling client.close(), client.destroy(), or a previous client.quit(). Calling quit() in an error handler that also runs after a normal shutdown path.

Common situations: Cleanup logic in finally blocks that runs after an error path already closed the client; signal handlers (SIGTERM) calling quit() on a client closed by a health-check failure; double-shutdown in test teardown.

Related errors


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

Appendix: source

Thrown at packages/client/lib/client/socket.ts:451

        if (this.#socket.writableNeedDrain) break;
      }
    } catch (err) {
      // net.Socket.write can throw synchronously on a half-closed socket
      // (writeAfterFIN -> EPIPE) before the 'close' event fires. The pending
      // command has already been moved to #waitingForReply by the queue's
      // generator, so the close handler will reject it on reconnect.
      if (!err || (err as NodeJS.ErrnoException).code !== 'EPIPE') {
        throw err;
      }
    } finally {
      this.#socket.uncork();
    }
  }

  async quit<T>(fn: () => Promise<T>): Promise<T> {
    if (!this.#isOpen) {
      throw new ClientClosedError();
    }

    this.#isOpen = false;
    const reply = await fn();
    this.destroySocket();
    return reply;
  }

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

    this.#isOpen = false;
  }

  destroy() {
    if (!this.#isOpen) {

View on GitHub (pinned to 90fd0652bc)