{"record":{"id":"9beddf23514cb470","repo":"hashicorp/terraform","slug":"localstate-not-locked","errorCode":null,"errorMessage":"LocalState not locked","messagePattern":"LocalState not locked","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/command/clistate/local_state.go","lineNumber":214,"sourceCode":"\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)\n}\n\nfunc (s *LocalState) Unlock(id string) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif s.lockID == \"\" {\n\t\treturn fmt.Errorf(\"LocalState not locked\")\n\t}\n\n\tif id != s.lockID {\n\t\tidErr := fmt.Errorf(\"invalid lock id: %q. current id: %q\", id, s.lockID)\n\t\tinfo, err := s.lockInfo()\n\t\tif err != nil {\n\t\t\tidErr = errors.Join(idErr, err)\n\t\t}\n\n\t\treturn &statemgr.LockError{\n\t\t\tErr:  idErr,\n\t\t\tInfo: info,\n\t\t}\n\t}\n\n\tos.Remove(s.lockInfoPath())\n\n\tfileName := s.stateFileOut.Name()","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/command/clistate/local_state.go#L196-L232","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n//   lockID, _ := state.Lock(info)\n//   defer state.Unlock(lockID)   // panics path: if Lock failed, lockID==\"\" -> Unlock errors\n// after\n//   lockID, err := state.Lock(info)\n//   if err != nil { return err }\n//   defer state.Unlock(lockID)","handlingStrategy":"validation","validationCode":"// Only unlock when a real lock was acquired\nlockID, err := state.Lock(info)\nif err != nil { return err }\ndefer func() {\n    if lockID != \"\" {\n        _ = state.Unlock(lockID)\n    }\n}()","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["state","locking","local-backend","api-misuse"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}