alibaba/nacos · error · IllegalStateException
Failed to unlock: {}
Error message
Failed to unlock: {} What it means
Thrown by NacosLock.unlock() when a NacosException (network/gRPC-level failure) occurs during the server RELEASE call. The catch block clears local state (reentrant count reset to 0, watchdog unregistered) to prevent the lock becoming permanently unusable, logs the original exception at ERROR level, and rethrows as an IllegalStateException wrapping the cause. This converts transport-layer NacosException into an unchecked IllegalStateException so callers' lock()/unlock() JUC contracts are consistent.
Source
Thrown at client/src/main/java/com/alibaba/nacos/client/lock/NacosLock.java:326
}
} else {
localReentrantCount.set(0);
watchdog.unregister(key);
localReentrantCount.remove();
removed = true;
throw new IllegalMonitorStateException(
"Unlock rejected by server, key=" + key + ", msg="
+ result.getErrorMessage());
}
} catch (NacosException e) {
// Server may have already released the lock — clear client state
// to prevent the lock from becoming permanently unusable.
localReentrantCount.set(0);
watchdog.unregister(key);
localReentrantCount.remove();
removed = true;
LOGGER.error("Failed to unlock, key={}", key, e);
throw new IllegalStateException("Failed to unlock: " + key, e);
}
} finally {
inUnlock.remove();
if (!removed && localReentrantCount.get() <= 0) {
localReentrantCount.remove();
}
}
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException(
"Condition not supported in Nacos distributed lock");
}
public String getKey() {
return key;
}View on GitHub (pinned to 9b989acdf1)
Solutions
- Catch IllegalStateException around unlock() and log it — local client state has already been cleared so no further cleanup is needed.
- Check the wrapped NacosException cause (e.getCause()) to see if it is a transient connection error; if so the server may or may not still hold the lease — rely on lease expiry on the server side.
- Verify the gRPC client connection is healthy before performing the critical section to reduce mid-release disconnects.
- Avoid calling unlock() after shutdown() of the NacosLockService, which would close the channel.
Example fix
// before
lock.unlock(); // IllegalStateException on transport error
// after
try {
lock.unlock();
} catch (IllegalStateException e) {
// local state already cleared by NacosLock; server may still hold lease until expiry
LOGGER.warn("Unlock transport failed for key={}, will rely on lease expiry", key, e);
} Defensive patterns
Strategy: try-catch
Try / catch
try { lock.unlock(); } catch (IllegalStateException e) { // transport failed; local state cleared; rely on server lease expiry LOGGER.warn("Unlock transport failed; will rely on lease expiry", e); } Prevention
- Do not call unlock() after NacosLockService.shutdown() — the gRPC channel is closed.
- Check the wrapped cause (e.getCause()) to distinguish transient network errors from permanent failures.
- Keep the critical section short to reduce the window for mid-operation disconnects.
When it happens
Trigger: gRPC channel disconnected or timed out during the unlock RPC; server returned an unexpected error code (not a clean success/fail LockResult but an error Response); the connection was torn down (e.g. client shutdown racing with unlock).
Common situations: Transient network failures during release; the LockGrpcClient or NacosLockService being shut down concurrently with an unlock call; gRPC stream reset; server restart mid-operation. Local state is always cleared, so the lock object can be reused after the error.
Related errors
- 500
- Failed to acquire lock: {}
- request timeout after {timeout} milliseconds, requestId={req
- Current thread does not hold the lock
- Unlock rejected by server, key={}, msg={}
AI-assisted analysis of alibaba/nacos@9b989acdf1 (2026-08-14).
Data as JSON: /api/errors/e86f892665ba70ed.
Report an issue: GitHub.