{"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":256,"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":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/cluster/cluster-slots.ts#L238-L274","documentation":"Thrown from RedisClusterSlots.#discoverWithRootNodes (cluster-slots.ts:256) inside the first half of the root-node scan loop, when #isOpen became false during an in-flight connect()/discovery. This means close()/destroy() ran concurrently with connect() — the discovery honors the teardown and aborts instead of resurrecting a closed cluster.","triggerScenarios":"cluster.connect() is in progress (iterating root nodes) and another code path calls cluster.close() or destroy() mid-discovery.","commonSituations":"Shutdown racing with startup; a health check starting a connect that is immediately cancelled; tests that tear down the cluster before discovery finishes.","solutions":["Serialize connect()/close(): do not destroy a cluster whose connect() has not resolved.","Await connect() (or its rejection) before calling close()/destroy().","In tests, await connect().catch(() => {}) in afterEach before destroying."],"exampleFix":"// before\nconst p = cluster.connect();\ncluster.destroy(); // races with discovery -> 'Cluster closed'\nawait p;\n\n// after\ntry { await cluster.connect(); } catch {}\nawait cluster.destroy();","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isClusterClosed(e: unknown): boolean {\n  return e instanceof Error && /Cluster closed/i.test(e.message);\n}","tryCatchPattern":"try { await cluster.connect(); }\ncatch (e) { if (isClusterClosed(e)) { /* expected: concurrent teardown */ return; } throw e; }","preventionTips":["Await connect() before calling close()/destroy().","Serialize startup and shutdown with a single lifecycle owner.","In tests, await connect().catch(()=>{}) in cleanup before destroy()."],"tags":["cluster","lifecycle","concurrency","startup"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}