{"record":{"id":"034df378d716a0e8","repo":"redis/node-redis","slug":"redissentinelclient-lease-already-released","errorCode":null,"errorMessage":"RedisSentinelClient lease already released","messagePattern":"RedisSentinelClient lease already released","errorType":"exception","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/90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58/packages/client/lib/sentinel/index.ts#L259-L295","documentation":"release() was called on a RedisSentinelClient lease that was already released. The #clientInfo field is already undefined (set by the first release), so a second call is rejected to prevent corrupting pool state or double-returning the lease.","triggerScenarios":"Calling lease.release() twice — e.g., an explicit release followed by a finally block that calls release again; error-handling code that releases then the normal path also releases.","commonSituations":"try/finally where release is in the finally and also in the try body; cleanup logic in multiple error handlers both calling release; async race where two code paths release the same lease.","solutions":["Guard release with a boolean flag to ensure it is called exactly once","Use try/finally with release only in the finally block, never in the try body","Null out the lease reference after release as a visual indicator it is spent"],"exampleFix":"// before\nconst lease = await sentinel.getClient();\nawait lease.release();\nawait lease.release(); // throws\n\n// after\nconst lease = await sentinel.getClient();\nlet released = false;\ntry {\n  // ... work\n} finally {\n  if (!released) {\n    released = true;\n    await lease.release();\n  }\n}","handlingStrategy":"validation","validationCode":"// Guard release with a boolean flag to prevent double-release.\nlet released = false;\nconst lease = await sentinel.getClient();\n\nfunction safeRelease() {\n  if (released) return;\n  released = true;\n  return lease.release();\n}\n\ntry {\n  // ... work\n} finally {\n  await safeRelease();\n}","typeGuard":null,"tryCatchPattern":"try {\n  await lease.release();\n} catch (e) {\n  if (e.message === 'RedisSentinelClient lease already released') {\n    // Already released — safe to ignore\n  }\n}","preventionTips":["Call release() exactly once, in a finally block","Use a boolean flag to guard against double-release in error paths","Null out the lease reference after release as a visual indicator","Avoid releasing in both the try body and the finally block"],"tags":["sentinel","lease","lifecycle","double-release"],"backgroundTag":null,"analyzedSha":"90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58","analyzedAt":"2026-08-11T15:37:21.243Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}