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

  1. Only call Unlock if Lock returned a non-empty lock ID — capture and check the ID.
  2. Guard deferred cleanup: `if lockID != "" { state.Unlock(lockID) }`.
  3. Do not call Unlock twice for the same lock; clear your local ID variable after unlocking.
  4. 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

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


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)