{"record":{"id":"495d41a4f1459fb2","repo":"plandex-ai/plandex","slug":"panic-in-gitremoveindexlockfileifexists-v-s","errorCode":null,"errorMessage":"panic in gitRemoveIndexLockFileIfExists: %v\n%s","messagePattern":"panic in gitRemoveIndexLockFileIfExists: (.+?)\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/git.go","lineNumber":620,"sourceCode":"}\n\nfunc gitRemoveIndexLockFileIfExists(repoDir string) error {\n\tlog.Printf(\"[Git] gitRemoveIndexLockFileIfExists - repoDir: %s\", repoDir)\n\n\tpaths := []string{\n\t\tfilepath.Join(repoDir, \".git\", \"index.lock\"),\n\t\tfilepath.Join(repoDir, \".git\", \"refs\", \"heads\", \"HEAD.lock\"),\n\t\tfilepath.Join(repoDir, \".git\", \"HEAD.lock\"),\n\t}\n\n\terrCh := make(chan error, len(paths))\n\n\tfor _, path := range paths {\n\t\tgo func(path string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in gitRemoveIndexLockFileIfExists: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in gitRemoveIndexLockFileIfExists: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tif err := removeLockFile(path); err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path)\n\t}\n\n\terrs := []error{}\n\tfor i := 0; i < len(paths); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}","sourceCodeStart":602,"sourceCodeEnd":638,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/git.go#L602-L638","documentation":"gitRemoveIndexLockFileIfExists spawns one goroutine per lock-file path, each with a recover() that converts any panic into this error (including the stack trace via debug.Stack()) sent to errCh. You see this error when one of the per-path goroutines panicked — almost always a nil-pointer/nil-map or similar bug in the removal path rather than a git or filesystem condition. The recover ensures the panic doesn't crash the process; runtime.Goexit() prevents a double-send to the channel.","triggerScenarios":"Any panic inside the deferred-recover-protected goroutine in gitRemoveIndexLockFileIfExists while it processes .git/index.lock, .git/refs/heads/HEAD.lock, or .git/HEAD.lock — e.g. a nil error value being dereferenced or channel misuse reaching the goroutine.","commonSituations":"A code change introduced a nil dereference or unchecked type assertion in removeLockFile/its helpers; running under unusual runtime conditions (stack exhaustion) surfaces latent panics; modified forked code passes invalid arguments (e.g. empty repoDir producing weird paths, though normally not a panic).","solutions":["Read the stack trace appended to the error message — it pinpoints the exact panicking function and line.","Fix the underlying nil pointer / bad type assertion at that line; this error is a symptom, not the root cause.","If caused by a recent change, diff removeLockFile / gitRemoveIndexLockFileIfExists against the last working version.","Confirm inputs are valid (non-empty repoDir that contains a .git directory) before invoking write operations."],"exampleFix":"// before: unchecked assumption inside goroutine work\nidx := someMap[\"key\"].(string) // panics if missing\n// after\nidx, ok := someMap[\"key\"].(string)\nif !ok {\n    errCh <- fmt.Errorf(\"missing key in map\")\n    return\n}","handlingStrategy":"try-catch","validationCode":"if repoDir == \"\" {\n    return errors.New(\"repoDir must be non-empty\")\n}\nif fi, err := os.Stat(filepath.Join(repoDir, \".git\")); err != nil || !fi.IsDir() {\n    return fmt.Errorf(\"%s is not a git repository\", repoDir)\n}","typeGuard":"func isPanicErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"panic in gitRemoveIndexLockFileIfExists\")\n}","tryCatchPattern":"if err := gitCommit(repoDir, msg); err != nil {\n    if isPanicErr(err) {\n        log.Printf(\"recovered panic in lock cleanup, stack: %s\", err)\n        return fmt.Errorf(\"internal bug in lock cleanup: %w\", err)\n    }\n    return err\n}","preventionTips":["Run go vet and race detector (go test -race) on code touching removeLockFile.","Never modify the goroutine/channel logic without re-reviewing the deferred recover and Goexit pattern.","Keep stack traces from these errors in logs — they are the only pointer to the true panic site.","Cover gitRemoveIndexLockFileIfExists with unit tests before refactoring it."],"tags":["go","panic","concurrency","git","goroutine"],"backgroundTag":"goroutine-panic-recovered","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}