{"id":"8ef074e5aee4405e","repo":"redis/node-redis","slug":"the-client-is-offline","errorCode":null,"errorMessage":"The client is offline","messagePattern":"The client is offline","errorType":"exception","errorClass":"ClientOfflineError","httpStatus":null,"severity":"error","filePath":"packages/client/lib/cluster/cluster-slots.ts","lineNumber":892,"sourceCode":"      promises.push(fn(this.pubSubNode.client));\n      this.pubSubNode = undefined;\n    }\n\n    this.#resetSlots();\n    this.nodeByAddress.clear();\n    this.#reconnectionTracker.clear();\n\n    await Promise.allSettled(promises);\n    this.#emit('disconnect');\n  }\n\n  #assertReady() {\n    if (!this.#isOpen) {\n      throw new ClientClosedError();\n    }\n\n    if (!this.#isReady) {\n      throw new ClientOfflineError();\n    }\n  }\n\n  /**\n   * All fan-out target nodes (masters + replicas), WITHOUT connecting. The\n   * caller connects each node lazily in its own per-node promise so a single\n   * failed connect rejects only that node's execution — letting reducers such\n   * as `one_succeeded` still see the reachable shards — instead of a `Promise.all`\n   * over the connects failing the whole route up front. Excludes the dedicated\n   * PubSub connection (not in `masters`/`replicas`).\n   */\n  getAllNodes() {\n    this.#assertReady();\n\n    return [...this.masters, ...this.replicas];\n  }\n\n  /** Master fan-out target nodes, WITHOUT connecting (see {@link getAllNodes}). */","sourceCodeStart":874,"sourceCodeEnd":910,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/cluster/cluster-slots.ts#L874-L910","documentation":"Thrown from RedisClusterSlots.#assertReady (cluster-slots.ts:892) as ClientOfflineError when #isOpen is true but #isReady is false. The cluster is open but not yet ready to route commands — typically because initial discovery is still running, or readiness was lost (e.g. destroyed mid-discovery resets isReady) and not yet re-established. Any routing/command method gated by assertReady rejects.","triggerScenarios":"Issuing a command after connect() started but before it resolved (isReady still false); issuing a command during a reconnect/topology refresh window where isReady has dropped.","commonSituations":"Forgetting to await connect(); racing the first command; issuing commands during a transient unready window after a topology event.","solutions":["Await cluster.connect() so #isReady is true before sending commands.","Before bursts of commands, check cluster.isReady and back off / retry if false.","For intermittent unready windows, retry the command with a short backoff."],"exampleFix":"// before\ncluster.connect(); // not awaited\nawait cluster.get('k'); // throws 'The client is offline'\n\n// after\nawait cluster.connect();\nawait cluster.get('k');","handlingStrategy":"validation","validationCode":"if (!cluster.isReady) { /* wait or back off */ }","typeGuard":"import { ClientOfflineError } from '@redis/client';\nfunction isClientOffline(e: unknown): boolean {\n  return e instanceof Error && (e instanceof ClientOfflineError || /client is offline/i.test(e.message));\n}","tryCatchPattern":"for (let i = 0; i < 5; i++) {\n  try { return await cluster.get('k'); }\n  catch (e) { if (isClientOffline(e) && i < 4) { await sleep(100 * 2 ** i); continue; } throw e; }\n}","preventionTips":["Await connect() so isReady is true before traffic.","Check cluster.isReady before bursts and back off if false.","Retry with backoff through transient unready windows."],"tags":["cluster","lifecycle","connection","readiness"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}