{"id":"1fdda211cb6fae9f","repo":"redis/node-redis","slug":"already-attempting-to-open","errorCode":null,"errorMessage":"already attempting to open","messagePattern":"already attempting to open","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/sentinel/index.ts","lineNumber":988,"sourceCode":"   *          if the client was immediately ready or no longer exists\n   */\n  releaseClientLease(clientInfo: ClientInfo) {\n    const client = this.#masterClients[clientInfo.id];\n    // client can be undefined if releasing in middle of a reconfigure\n    if (client !== undefined) {\n      const dirtyPromise = client.resetIfDirty();\n      if (dirtyPromise) {\n        return dirtyPromise\n          .then(() => this.#masterClientQueue.push(clientInfo.id));\n      }\n    }\n\n    this.#masterClientQueue.push(clientInfo.id);\n  }\n\n  async connect() {\n    if (this.#isOpen) {\n      throw new Error(\"already attempting to open\")\n    }\n\n    try {\n      this.#isOpen = true;\n\n      this.#connectPromise = this.#connect();\n      await this.#connectPromise;\n      this.#isReady = true;\n    } catch (err) {\n      // The initial connect gave up. Tear down whatever was created along the\n      // way: clients whose first connection attempt failed keep reconnecting\n      // per their `reconnectStrategy`, and would otherwise stay alive in the\n      // background (holding sockets and timers) after `connect()` rejected.\n      this.#connectPromise = undefined;\n      await this.destroy();\n      throw err;\n    } finally {\n      this.#connectPromise = undefined;","sourceCodeStart":970,"sourceCodeEnd":1006,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/sentinel/index.ts#L970-L1006","documentation":"Thrown by RedisSentinelInternal.connect() when the `#isOpen` flag is already true. connect() is a one-shot lifecycle entry point: it sets #isOpen, drives #connect(), and only clears it in the finally block. Calling connect() a second time while the first is still in flight (or after a successful open) violates the state machine and is rejected immediately (sentinel/index.ts:987).","triggerScenarios":"Calling sentinel.connect() twice without awaiting the first; calling connect() again after it already resolved; a reconnect/retry wrapper that invokes connect() on top of an already-open client.","commonSituations":"Retry/early-out wrappers around connect() that don't track the promise; calling connect() in both a bootstrap function and a health check; tests that reuse a client instance across cases.","solutions":["Await the original connect() promise and store it; reuse it instead of calling connect() again.","Guard with `if (!sentinel.isOpen) await sentinel.connect();` before connecting.","If you need a fresh connection after an error, call destroy() first, then connect().","Memoize the connect promise at the call site: `this._ready ??= sentinel.connect();`."],"exampleFix":"// before\nawait sentinel.connect(); // may run twice on retry\n// after\nif (!sentinel.isOpen) {\n  await sentinel.connect();\n}","handlingStrategy":"validation","validationCode":"let readyPromise: Promise<void> | undefined;\nasync function ensureOpen(sentinel) {\n  if (sentinel.isOpen) return;\n  readyPromise ??= sentinel.connect();\n  try {\n    await readyPromise;\n  } finally {\n    readyPromise = undefined;\n  }\n}","typeGuard":"const isOpen = (s: { isOpen: boolean }): boolean => s.isOpen;","tryCatchPattern":null,"preventionTips":["Memoize the connect() promise so concurrent callers await the same one.","Guard every connect() call site with `if (!sentinel.isOpen)`.","Call destroy() before re-connecting on a recovered error.","Never put connect() inside an automatic retry loop that could overlap calls."],"tags":["lifecycle","sentinel","concurrency"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}