{"id":"034df378d716a0e8","repo":"redis/node-redis","slug":"redissentinelclient-lease-already-released","errorCode":null,"errorMessage":"RedisSentinelClient lease already released","messagePattern":"RedisSentinelClient lease already released","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/sentinel/index.ts","lineNumber":277,"sourceCode":"      client => client.unwatch()\n    )\n  }\n\n  unwatch = this.UNWATCH;\n\n  /**\n   * Releases the client lease back to the pool\n   *\n   * After calling this method, the client instance should no longer be used as it\n   * will be returned to the client pool and may be given to other operations.\n   *\n   * @returns A promise that resolves when the client is ready to be reused, or undefined\n   *          if the client was immediately ready\n   * @throws Error if the lease has already been released\n   */\n  release() {\n    if (this._self.#clientInfo === undefined) {\n      throw new Error('RedisSentinelClient lease already released');\n    }\n\n    const result = this._self.#internal.releaseClientLease(this._self.#clientInfo);\n    this._self.#clientInfo = undefined;\n    return result;\n  }\n}\n\nexport default class RedisSentinel<\n  M extends RedisModules,\n  F extends RedisFunctions,\n  S extends RedisScripts,\n  RESP extends RespVersions,\n  TYPE_MAPPING extends TypeMapping\n> extends EventEmitter {\n  readonly _self: RedisSentinel<M, F, S, RESP, TYPE_MAPPING>;\n\n  #internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>;","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/sentinel/index.ts#L259-L295","documentation":"RedisSentinelClient.release() is idempotent-violating by design: calling it when #clientInfo is already undefined throws, because a double release would return the same master pool slot twice and corrupt the pool accounting. The lease is a single-use resource.","triggerScenarios":"Calling release() twice on the same sentinel lease handle; calling release() in a finally block and again in a caller's finally; releasing the reserved client obtained via reserveClient:true twice.","commonSituations":"Nested try/finally blocks both releasing; acquire() result passed to a helper that releases and the caller also releases; error handling that releases then re-throws into another release.","solutions":["Release exactly once; track a released flag if multiple code paths could call it.","Prefer sentinel.use(fn) which handles acquire/release for you and cannot double-release.","Null out the reference after release() to make accidental reuse obvious.","Ensure finally blocks are not nested around the same lease."],"exampleFix":"// before\nconst c = await sentinel.acquire();\ntry { ... } finally { await c.release(); }\nawait c.release(); // double release throws\n\n// after\nlet released = false;\nconst c = await sentinel.acquire();\ntry { ... } finally { if (!released) { await c.release(); released = true; } }","handlingStrategy":"validation","validationCode":"let released = false;\nconst releaseOnce = async (c) => { if (!released) { released = true; await c.release(); } };","typeGuard":null,"tryCatchPattern":"try { await c.release(); } catch (e) { if (!/already released/.test(e.message)) throw e; }","preventionTips":["Prefer sentinel.use(fn) to avoid manual release entirely.","Track a released flag when multiple paths can release.","Null the handle after release to make reuse obvious."],"tags":["sentinel","lease","double-release","lifecycle","pool"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}