hashicorp/terraform · error
state already locked
Error message
state %q already locked
What it means
Thrown by LocalState.Lock when a lock is requested but s.lockID is already non-empty — i.e. this LocalState instance already holds a lock on the state file. Terraform local backend state locking is in-process (mu mutex) plus on-disk (.tflock file); this specific error is the in-process re-entrancy guard, not the on-disk contention path (that goes through s.lock()).
Solutions
- Ensure Lock is paired with Unlock; track lock ownership in the caller.
- If unsure of current state, check the returned lock ID and call Unlock with it before re-locking.
- For concurrent operations, serialize access at the orchestrator level rather than relying on double-Lock to no-op.
- If a previous run crashed leaving the .tflock file on disk, run `terraform force-unlock <id>` to clear stale on-disk locks (separate from this in-process guard).
Defensive patterns
Strategy: validation
Validate before calling
func (s *LocalState) lockHeld() bool { return s.lockID != "" } Prevention
- Always pair Lock with Unlock; track the ID in the caller.
- Never call Lock twice on the same instance; serialize operations.
- In wrappers, branch on whether the lock is already held.
When it happens
Trigger: Calling Lock twice on the same LocalState instance without Unlock in between; a code path that retries Lock after a partial success; programmatic embedding that double-locks.
Common situations: Custom automation that calls terraform Lock explicitly twice; a wrapper that re-invokes an operation before the previous Unlock completed; a bug in a higher-level command flow that forgets it already locked.
Related errors
- invalid lock id: . current id
- could not write lock info for
- LocalState not locked
- state file locked, but could not unmarshal lock info
- backendDiags.Err()
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/9fb1f6a2187d5be0.
Report an issue: GitHub.
Appendix: source
Thrown at internal/command/clistate/local_state.go:188
s.state = state
s.readState = s.state.DeepCopy()
return nil
}
// Lock implements a local filesystem state.Locker.
func (s *LocalState) Lock(info *statemgr.LockInfo) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.stateFileOut == nil {
if err := s.createStateFiles(); err != nil {
return "", err
}
}
if s.lockID != "" {
return "", fmt.Errorf("state %q already locked", s.stateFileOut.Name())
}
if err := s.lock(); err != nil {
info, infoErr := s.lockInfo()
if infoErr != nil {
err = errors.Join(err, infoErr)
}
lockErr := &statemgr.LockError{
Info: info,
Err: err,
}
return "", lockErr
}
s.lockID = info.ID
return s.lockID, s.writeLockInfo(info)View on GitHub (pinned to d32a084675)