grpc/grpc-java · error · IllegalStateException

Can't cancel resource watch with active watchers present

Error message

Can't cancel resource watch with active watchers present

What it means

XdsClientImpl throws this IllegalStateException when cancelResourceWatch() is called on a resource that still has active watchers registered. A resource watch can only be unsubscribed once every watcher has been cancelled; the internal invariant is that isWatched() must be false before teardown of the subscription and its response timer.

Solutions

  1. Ensure all XdsClient.Watcher instances for the resource are cancelled (via their handles) before calling cancelResourceWatch
  2. Check for duplicate registrations that leave a stale watcher alive; only cancel once per watcher
  3. Guard the cancel path so it only runs when your component owns the last watcher
  4. If it appears during shutdown races, synchronize watcher registration/cancellation on a single owner

Example fix

// before
client.cancelResourceWatch(resourceName); // IllegalStateException: watchers still active
// after
watchHandle1.cancel();
watchHandle2.cancel();
client.cancelResourceWatch(resourceName); // safe once isWatched() is false
Defensive patterns

Strategy: try-catch

Validate before calling

// Track your own watcher count per resource
if (activeWatchers.get(resourceName) > 0) { throw new IllegalStateException("cancel watchers first"); }

Type guard

boolean safeToCancel = activeWatchers.containsKey(resourceName) && activeWatchers.get(resourceName) == 0;

Try / catch

try {
  client.cancelResourceWatch(resourceName);
} catch (IllegalStateException e) {
  // a watcher is still active; cancel remaining watcher handles then retry
}

Prevention

When it happens

Trigger: Calling XdsClient.cancelResourceWatch() (or internally via watch teardown) while other components still hold watchers for the same resource name on the same XdsClient.

Common situations: Multiple subscribers share one XdsClient (e.g. multiple gRPC channels using xDS for the same cluster/listener resource) and one cancels without the others; lifecycle bugs where a watcher is re-registered during shutdown; races between cancel and re-watch.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/36bc3ec04b1aba13. Report an issue: GitHub.

Appendix: source

Thrown at xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java:808

      metadata = ResourceMetadata.newResourceMetadataRequested();

      if (respTimer != null) {
        respTimer.cancel();
      }
      respTimer = syncContext.schedule(
          new ResourceNotFound(), timeoutSec, TimeUnit.SECONDS, timeService);
    }

    void stopTimer() {
      if (respTimer != null && respTimer.isPending()) {
        respTimer.cancel();
        respTimer = null;
      }
    }

    void cancelResourceWatch() {
      if (isWatched()) {
        throw new IllegalStateException("Can't cancel resource watch with active watchers present");
      }
      stopTimer();
      String message = "Unsubscribing {0} resource {1} from server {2}";
      XdsLogLevel logLevel = XdsLogLevel.INFO;
      if (resourceDeletionIgnored) {
        message += " for which we previously ignored a deletion";
        logLevel = XdsLogLevel.FORCE_INFO;
      }
      logger.log(logLevel, message, type, resource, getTarget());
    }

    boolean isWatched() {
      return !watchers.isEmpty();
    }

    boolean hasResult() {
      return data != null || absent;
    }

View on GitHub (pinned to 64daddc1f3)