{"record":{"id":"234463fb2e6c4e1b","repo":"plandex-ai/plandex","slug":"panic-in-getplandiffs-v-n-s","errorCode":null,"errorMessage":"panic in GetPlanDiffs: %v\\n%s","messagePattern":"panic in GetPlanDiffs: (.+?)\\\\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/diff_helpers.go","lineNumber":55,"sourceCode":"\terr = initGitRepo(tempDirPath)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error initializing git repo: %v\", err)\n\t}\n\n\tfiles := planState.CurrentPlanFiles.Files\n\tremoved := planState.CurrentPlanFiles.Removed\n\n\t// write the original files to the temp dir\n\terrCh := make(chan error, len(planState.ContextsByPath))\n\thasAnyOriginal := false\n\n\tfor path, context := range planState.ContextsByPath {\n\t\tgo func(path string, context *shared.Context) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetPlanDiffs: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanDiffs: %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\t_, hasPath := files[path]\n\t\t\t_, hasRemoved := removed[path]\n\t\t\tif hasPath || hasRemoved {\n\t\t\t\thasAnyOriginal = true\n\t\t\t\t// ensure file directory exists\n\t\t\t\terr = os.MkdirAll(filepath.Dir(filepath.Join(tempDirPath, path)), 0755)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error creating directory: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\terr = os.WriteFile(filepath.Join(tempDirPath, path), []byte(context.Body), 0644)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error writing file: %v\", err)\n\t\t\t\t\treturn","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/diff_helpers.go#L37-L73","documentation":"Each per-path goroutine inside GetPlanDiffs has a deferred recover() so a panic while processing one file context does not crash the server. Instead the panic value and stack trace are logged, converted to an error, and sent on errCh; runtime.Goexit() stops the goroutine so nothing else is sent. This error surfaces through the outer loop as 'error writing original files to temp dir: panic in GetPlanDiffs: ...'.","triggerScenarios":"A per-path worker goroutine panics (e.g. nil pointer dereference if a *shared.Context is nil, or an out-of-range/nil map access) while building diffs for planState.ContextsByPath.","commonSituations":"Plan state contains a context entry whose Body or pointer is nil due to corrupted/incomplete persisted state; concurrent mutation of planState while goroutines read it; a regression in the loop body code.","solutions":["Read the stack trace embedded in the message to find the nil/invalid value; inspect the offending path's context in the plan state.","Harden the goroutine body: nil-check context and context.Body before use, and validate planState maps.","If plan state is corrupted, repair or re-load it via GetCurrentPlanState or restore from backup.","Check for data races on planState/files/removed shared with the goroutines and add synchronization or copies."],"exampleFix":"// before\n_, hasPath := files[path]\n// after (nil-safe)\nif context == nil {\n    errCh <- fmt.Errorf(\"nil context for path %s\", path)\n    return\n}\n_, hasPath := files[path]","handlingStrategy":"try-catch","validationCode":"for path, ctx := range planState.ContextsByPath {\n    if ctx == nil {\n        return fmt.Errorf(\"nil context for path %q in plan state\", path)\n    }\n}","typeGuard":"func isValidContext(c *shared.Context) bool {\n    return c != nil\n}","tryCatchPattern":"// already guarded in-library; at the call site:\nout, err := GetPlanDiffs(orgId, planId, plain)\nif err != nil && strings.HasPrefix(err.Error(), \"error writing original files to temp dir: panic in GetPlanDiffs\") {\n    log.Printf(\"worker panic while diffing plan %s: %v\", planId, err)\n    // treat plan state as suspect: reload or surface to user\n}","preventionTips":["Nil-check context pointers before spawning worker goroutines.","Never share/mutate planState maps while goroutines read them; snapshot or synchronize.","Keep the recover()+Goexit pattern intact in any new goroutine code so panics become errors.","Add tests covering plan state with missing/nil contexts."],"tags":["panic","goroutine","concurrency"],"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-14T00:17:10.932Z"}