{"id":"4c19fa3176de3002","repo":"redis/node-redis","slug":"attempted-execution-on-released-redissentinelclien","errorCode":null,"errorMessage":"Attempted execution on released RedisSentinelClient lease","messagePattern":"Attempted execution on released RedisSentinelClient lease","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/sentinel/index.ts","lineNumber":184,"sourceCode":"      S,\n      RESP,\n      K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING\n    >;\n  }\n\n  /**\n   * Override the `typeMapping` command option\n   */\n  withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {\n    return this._commandOptionsProxy('typeMapping', typeMapping);\n  }\n\n  async _execute<T>(\n    isReadonly: boolean | undefined,\n    fn: (client: RedisClient<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>) => Promise<T>\n  ): Promise<T> {\n    if (this._self.#clientInfo === undefined) {\n      throw new Error(\"Attempted execution on released RedisSentinelClient lease\");\n    }\n\n    return await this._self.#internal.execute(fn, this._self.#clientInfo);\n  }\n\n  async sendCommand<T = ReplyUnion>(\n    isReadonly: boolean | undefined,\n    args: CommandArguments,\n    options?: CommandOptions,\n  ): Promise<T> {\n    const mergedOptions = { ...this.commandOptions, ...options };\n    return this._execute(\n      isReadonly,\n      client => client.sendCommand(args, mergedOptions)\n    );\n  }\n\n  /**","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/sentinel/index.ts#L166-L202","documentation":"RedisSentinelClient._execute guards against use after release: once release() sets the internal #clientInfo to undefined, any subsequent command routed through _execute throws. The leased sentinel client is a short-lived handle tied to a master pool slot; after release it has no backing client and must not be used.","triggerScenarios":"Calling any command on a RedisSentinelClient obtained via sentinel.use(fn) or sentinel.acquire() after the callback returned or release() was called. Using a stale reference captured before a topology reset.","commonSituations":"Forgetting that use()'s callback is the only safe scope; storing the leased client in a variable and calling it later; double-using a client after an error path already released it; async code holding the reference across an await that follows release.","solutions":["Issue all commands inside the sentinel.use(async c => {...}) callback and never leak c out of it.","If using acquire(), call release() exactly once and never touch the client afterward.","Do not cache the leased client in a long-lived variable.","After an error, obtain a fresh lease rather than reusing the old handle."],"exampleFix":"// before\nconst c = await sentinel.acquire();\nawait c.release();\nawait c.get('k'); // throws\n\n// after\nawait sentinel.use(async c => { await c.get('k'); }); // scope owns the lease","handlingStrategy":"validation","validationCode":"await sentinel.use(async c => { await c.get('k'); }); // scope guarantees liveness","typeGuard":"function leaseAlive(c) { return c != null && typeof c.release === 'function' && c.isOpen; }","tryCatchPattern":"// Not a catch scenario — design the lease to be used only inside use()/acquire()-release.","preventionTips":["Use sentinel.use(fn) so the lease never escapes scope.","Treat release() as terminal; never call the handle again.","Obtain a fresh lease after any error instead of reusing."],"tags":["sentinel","lease","use-after-release","lifecycle"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}