{"record":{"id":"9fb1f6a2187d5be0","repo":"hashicorp/terraform","slug":"state-q-already-locked-9fb1f6","errorCode":null,"errorMessage":"state %q already locked","messagePattern":"state %q already locked","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/command/clistate/local_state.go","lineNumber":188,"sourceCode":"\n\ts.state = state\n\ts.readState = s.state.DeepCopy()\n\treturn nil\n}\n\n// Lock implements a local filesystem state.Locker.\nfunc (s *LocalState) Lock(info *statemgr.LockInfo) (string, error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif s.stateFileOut == nil {\n\t\tif err := s.createStateFiles(); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif s.lockID != \"\" {\n\t\treturn \"\", fmt.Errorf(\"state %q already locked\", s.stateFileOut.Name())\n\t}\n\n\tif err := s.lock(); err != nil {\n\t\tinfo, infoErr := s.lockInfo()\n\t\tif infoErr != nil {\n\t\t\terr = errors.Join(err, infoErr)\n\t\t}\n\n\t\tlockErr := &statemgr.LockError{\n\t\t\tInfo: info,\n\t\t\tErr:  err,\n\t\t}\n\n\t\treturn \"\", lockErr\n\t}\n\n\ts.lockID = info.ID\n\treturn s.lockID, s.writeLockInfo(info)","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/command/clistate/local_state.go#L170-L206","documentation":"Returned by LocalState.Lock (local_state.go:177) when an in-process lock is already held (s.lockID != \"\"). LocalState uses both an in-memory mutex (s.mu) and an on-disk lock file to coordinate access; this specific guard prevents the same LocalState instance from being locked twice without an intervening Unlock. The stateFileOut.Name() is included so the caller knows which state file is contested.","triggerScenarios":"Calling Lock twice on the same *LocalState without Unlock in between; a wrapper that retries Lock on transient errors without first unwinding the previous (partially) successful lock; calling Lock from multiple goroutines sharing one LocalState pointer; a state manager whose Unlock path returned early and left s.lockID set.","commonSituations":"A test helper that Locks, hits an error, then Locks again in cleanup; an embedder reusing the same LocalState across `plan` and `apply` without unlocking; a custom backend that wraps LocalState and double-locks on retry; concurrent CLI processes are NOT the cause here — this guard is per-instance.","solutions":["Ensure every Lock is paired with a deferred Unlock so the same LocalState instance is never locked twice: `id, err := s.Lock(info); defer s.Unlock(id)`.","Before re-locking, check the returned lock id — if non-empty, call Unlock first.","Restructure retry logic to unlock fully before re-attempting Lock.","If sharing a LocalState across goroutines, serialize access through a single owner rather than locking from multiple goroutines."],"exampleFix":"// before\nid, _ := s.Lock(info)\n// ... error path forgets to unlock ...\nid2, err := s.Lock(info) // state \"terraform.tfstate\" already locked\n\n// after\nid, err := s.Lock(info)\nif err != nil { return err }\ndefer s.Unlock(id)\n// ... proceed ...","handlingStrategy":"validation","validationCode":"// Only call Lock if the instance is not already locked\nif s.lockID != \"\" {\n    return errors.New(\"refusing to Lock: this LocalState is already locked; call Unlock first\")\n}\nreturn s.Lock(info)","typeGuard":"// IsLocked reports whether this LocalState instance currently holds the in-process lock.\nfunc (s *LocalState) IsLocked() bool {\n    s.mu.Lock()\n    defer s.mu.Unlock()\n    return s.lockID != \"\"\n}","tryCatchPattern":null,"preventionTips":["Pair every Lock with a deferred Unlock at the call site.","Never retry Lock without an intervening Unlock.","Give the LocalState a single owner goroutine; do not share across goroutines.","Assert s.lockID == \"\" in tests before calling Lock."],"tags":["state","locking","concurrency","local-backend"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}