{"record":{"id":"0da713a08cbbaf44","repo":"redis/redis-rb","slug":"can-t-unsubscribe-if-not-subscribed","errorCode":null,"errorMessage":"Can't unsubscribe if not subscribed.","messagePattern":"Can't unsubscribe if not subscribed\\.","errorType":"exception","errorClass":"Redis::SubscriptionError","httpStatus":null,"severity":"error","filePath":"lib/redis/distributed.rb","lineNumber":1281,"sourceCode":"      !!@subscribed_node\n    end\n\n    # Listen for messages published to the given channels.\n    def subscribe(channel, *channels, &block)\n      if channels.empty?\n        @subscribed_node = node_for(channel)\n        @subscribed_node.subscribe(channel, &block)\n      else\n        ensure_same_node(:subscribe, [channel] + channels) do |node|\n          @subscribed_node = node\n          node.subscribe(channel, *channels, &block)\n        end\n      end\n    end\n\n    # Stop listening for messages posted to the given channels.\n    def unsubscribe(*channels)\n      raise SubscriptionError, \"Can't unsubscribe if not subscribed.\" unless subscribed?\n\n      @subscribed_node.unsubscribe(*channels)\n    end\n\n    # Listen for messages published to channels matching the given patterns.\n    # See the [Redis Server PSUBSCRIBE documentation](https://redis.io/docs/latest/commands/psubscribe/)\n    # for further details\n    def psubscribe(*channels, &block)\n      raise NotImplementedError\n    end\n\n    # Stop listening for messages posted to channels matching the given\n    # patterns.\n    # See the [Redis Server PUNSUBSCRIBE documentation](https://redis.io/docs/latest/commands/punsubscribe/)\n    # for further details\n    def punsubscribe(*channels)\n      raise NotImplementedError\n    end","sourceCodeStart":1263,"sourceCodeEnd":1299,"githubUrl":"https://github.com/redis/redis-rb/blob/2ba9010b91dab9e0fde1fbae3a9aae003f8bc307/lib/redis/distributed.rb#L1263-L1299","documentation":"Redis::Distributed tracks at most one active subscription via @subscribed_node, set by #subscribe and nil otherwise. #unsubscribe forwards to that node, so calling it when @subscribed_node is nil raises Redis::SubscriptionError (\"Can't unsubscribe if not subscribed.\") — the instance has no subscription to tear down. The same error class and message are used by the standalone client's subscription loop, so it predates Distributed. On Distributed it is purely a client-side state check: nothing is sent to any server.","triggerScenarios":"Calling dist.unsubscribe(*channels) before any dist.subscribe on that instance; after the subscribe block has already returned (subscription already closed); after an error interrupted the subscribe loop; in an ensure/cleanup block that runs whether or not a subscription exists; calling unsubscribe on a different Redis::Distributed instance than the one that subscribed.","commonSituations":"Worker shutdown hooks (SIGTERM handlers) that unconditionally unsubscribe; drain/cleanup code in ensure blocks; reconnect logic that tears down subscriptions it never established; multi-process forking where the child inherits the object but not the subscription state.","solutions":["Guard the call: dist.unsubscribe(*channels) if dist.subscribed?","Make the unsubscribe path use the same Distributed instance that subscribed (pass the client through, or store it next to the subscription)","For cleanup blocks, rescue Redis::SubscriptionError and treat it as already-clean"],"exampleFix":"# before\ndef shutdown(dist)\n  dist.unsubscribe(\"news\")\nend\n# => Redis::SubscriptionError: Can't unsubscribe if not subscribed.\n\n# after\ndef shutdown(dist)\n  dist.unsubscribe(\"news\") if dist.subscribed?\nend","handlingStrategy":"validation","validationCode":"# Distributed exposes subscribed? exactly for this check\ndist.unsubscribe(*channels) if dist.subscribed?","typeGuard":"def can_unsubscribe?(client)\n  !client.respond_to?(:subscribed?) || client.subscribed?\nend","tryCatchPattern":"begin\n  dist.unsubscribe(*channels)\nrescue Redis::SubscriptionError\n  # already unsubscribed — nothing to do\nend","preventionTips":["Always pair subscribe and unsubscribe on the same Distributed instance; store the client next to the subscription state","Guard every cleanup/ensure path that touches subscriptions with subscribed?","Remember subscribe with a block blocks until the subscription ends — code after the block does not need to unsubscribe at all","In shutdown hooks, treat SubscriptionError as benign"],"tags":["redis","ruby","distributed","pubsub","subscription","state-error","cleanup"],"backgroundTag":"unsubscribe-without-subscription","analyzedSha":"2ba9010b91dab9e0fde1fbae3a9aae003f8bc307","analyzedAt":"2026-08-23T03:54:57.017Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}