hashicorp/terraform · error
LocalState not locked
Error message
LocalState not locked
What it means
Thrown by LocalState.Unlock when the caller asks to release a lock but s.lockID is empty — there is no in-process lock to release. Unlock is meant to be the symmetric counterpart to a successful Lock; calling it without one is a programming error in the caller.
Solutions
- Only call Unlock if Lock returned a non-empty lock ID — capture and check the ID.
- Guard deferred cleanup: `if lockID != "" { state.Unlock(lockID) }`.
- Do not call Unlock twice for the same lock; clear your local ID variable after unlocking.
- In error paths, branch on whether Lock actually succeeded before attempting Unlock.
Example fix
// before
// lockID, _ := state.Lock(info)
// defer state.Unlock(lockID) // panics path: if Lock failed, lockID=="" -> Unlock errors
// after
// lockID, err := state.Lock(info)
// if err != nil { return err }
// defer state.Unlock(lockID) Defensive patterns
Strategy: validation
Validate before calling
// Only unlock when a real lock was acquired
lockID, err := state.Lock(info)
if err != nil { return err }
defer func() {
if lockID != "" {
_ = state.Unlock(lockID)
}
}() Prevention
- Guard deferred Unlock with a non-empty-ID check.
- Never call Unlock after a failed Lock.
- Set lockID to empty in your caller right after Unlock to prevent double-unlock.
When it happens
Trigger: Calling Unlock without a prior successful Lock on the same LocalState; calling Unlock twice (the second call sees lockID already cleared); an error-handling path that calls Unlock defensively even though Lock failed.
Common situations: Defensive cleanup code that calls Unlock in a defer regardless of whether Lock succeeded; a retry loop that double-unlocks; an exception path where Lock returned an error (so lockID was never set) but cleanup still calls Unlock.
Related errors
- could not write lock info for
- invalid lock id: . current id
- state file locked, but could not unmarshal lock info
- state already locked
- backendDiags.Err()
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/9beddf23514cb470.
Report an issue: GitHub.
Appendix: source
Thrown at internal/command/clistate/local_state.go:214
lockErr := &statemgr.LockError{
Info: info,
Err: err,
}
return "", lockErr
}
s.lockID = info.ID
return s.lockID, s.writeLockInfo(info)
}
func (s *LocalState) Unlock(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.lockID == "" {
return fmt.Errorf("LocalState not locked")
}
if id != s.lockID {
idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID)
info, err := s.lockInfo()
if err != nil {
idErr = errors.Join(idErr, err)
}
return &statemgr.LockError{
Err: idErr,
Info: info,
}
}
os.Remove(s.lockInfoPath())
fileName := s.stateFileOut.Name()View on GitHub (pinned to d32a084675)