{"record":{"id":"8658a9c98287075a","repo":"redis/node-redis","slug":"cluster-closed","errorCode":null,"errorMessage":"Cluster closed","messagePattern":"Cluster closed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/cluster/cluster-slots.ts","lineNumber":325,"sourceCode":"      await this.#discoverWithRootNodes();\n      // `destroy()` may have run while discovery was in flight; if so, this\n      // resolution is stale and must not resurrect readiness for a session\n      // that's already been torn down.\n      if (this.#isOpen) {\n        this.#isReady = true;\n        this.#emit('connect');\n      }\n    } catch (err) {\n      this.#isOpen = false;\n      this.#isReady = false;\n      throw err;\n    }\n  }\n\n  async #discoverWithRootNodes() {\n    const start = Math.floor(Math.random() * this.#options.rootNodes.length);\n    for (let i = start; i < this.#options.rootNodes.length; i++) {\n      if (!this.#isOpen) throw new Error('Cluster closed');\n      if (await this.#discover(this.#options.rootNodes[i])) {\n        return;\n      }\n    }\n\n    for (let i = 0; i < start; i++) {\n      if (!this.#isOpen) throw new Error('Cluster closed');\n      if (await this.#discover(this.#options.rootNodes[i])) {\n        return;\n      }\n    }\n\n    throw new RootNodesUnavailableError();\n  }\n\n  #resetSlots() {\n    this.slots = new Array(RedisClusterSlots.#SLOTS);\n    this.masters = [];","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/redis/node-redis/blob/90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58/packages/client/lib/cluster/cluster-slots.ts#L307-L343","documentation":"Thrown inside #discoverWithRootNodes during the forward iteration over rootNodes when #isOpen becomes false mid-discovery. This guard ensures that if the cluster is closed/destroyed while initial topology discovery is still trying root nodes, discovery stops immediately rather than continuing to connect to nodes for a cluster that no longer exists.","triggerScenarios":"Calling cluster.connect() and then, while it is still iterating root nodes (e.g., the first root is slow/unreachable), calling cluster.destroy() or cluster.disconnect(). The in-flight discover loop sees #isOpen flip to false and throws.","commonSituations":"Application startup racing with shutdown (e.g., SIGTERM during slow cluster discovery); a health check timing out and triggering destroy() while connect() is still resolving; test teardown cancelling a slow connect().","solutions":["Let the in-flight connect() reject and catch it in your startup code rather than suppressing.","Avoid calling destroy()/disconnect() while connect() is unresolved; await connect() first (or its rejection).","If you must cancel, catch the resulting 'Cluster closed' error and treat it as expected during shutdown.","Reduce connect latency with shorter connectTimeout so discovery fails fast instead of hanging."],"exampleFix":"// before\nconst p = cluster.connect();\ncluster.destroy(); // mid-discovery -> 'Cluster closed'\nawait p;\n\n// after\ntry {\n  await cluster.connect();\n} catch (e) {\n  // connect was cancelled or cluster torn down during discovery\n  if (!cluster.isOpen) return;\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"function isClusterClosedError(e: unknown): boolean {\n  return e instanceof Error && e.message === 'Cluster closed';\n}","tryCatchPattern":"try {\n  await cluster.connect();\n} catch (e) {\n  if (e instanceof Error && e.message === 'Cluster closed') return; // torn down mid-discovery\n  throw e;\n}","preventionTips":["Await connect() before initiating destroy()/disconnect().","Treat 'Cluster closed' from connect() as expected during shutdown.","Use a short connectTimeout so discovery fails fast."],"tags":["cluster","lifecycle","discovery","concurrency"],"backgroundTag":null,"analyzedSha":"90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58","analyzedAt":"2026-08-11T15:37:21.243Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}