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
- Ensure all XdsClient.Watcher instances for the resource are cancelled (via their handles) before calling cancelResourceWatch
- Check for duplicate registrations that leave a stale watcher alive; only cancel once per watcher
- Guard the cancel path so it only runs when your component owns the last watcher
- 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
- Cancel every Watcher handle before unsubscribing the resource
- Keep a single owner component responsible for watch lifecycle
- Avoid re-registering watchers during shutdown
- Log watcher add/remove to spot leaks
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
- A terminal HttpFilter must be the last filter
- Address is not an IP
- All xds transports for authority are in backoff
- AndMatcher must have at least 2 predicates
- ca_certificate_provider_instance name
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)