plandex-ai/plandex · error

panic in GetPlanDiffs: %v\n%s

Error message

panic in GetPlanDiffs: %v\n%s

What it means

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: ...'.

Source

Thrown at app/server/db/diff_helpers.go:55

	err = initGitRepo(tempDirPath)

	if err != nil {
		return "", fmt.Errorf("error initializing git repo: %v", err)
	}

	files := planState.CurrentPlanFiles.Files
	removed := planState.CurrentPlanFiles.Removed

	// write the original files to the temp dir
	errCh := make(chan error, len(planState.ContextsByPath))
	hasAnyOriginal := false

	for path, context := range planState.ContextsByPath {
		go func(path string, context *shared.Context) {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("panic in GetPlanDiffs: %v\n%s", r, debug.Stack())
					errCh <- fmt.Errorf("panic in GetPlanDiffs: %v\n%s", r, debug.Stack())
					runtime.Goexit() // don't allow outer function to continue and double-send to channel
				}
			}()
			_, hasPath := files[path]
			_, hasRemoved := removed[path]
			if hasPath || hasRemoved {
				hasAnyOriginal = true
				// ensure file directory exists
				err = os.MkdirAll(filepath.Dir(filepath.Join(tempDirPath, path)), 0755)
				if err != nil {
					errCh <- fmt.Errorf("error creating directory: %v", err)
					return
				}

				err = os.WriteFile(filepath.Join(tempDirPath, path), []byte(context.Body), 0644)
				if err != nil {
					errCh <- fmt.Errorf("error writing file: %v", err)
					return

View on GitHub (pinned to e2d772072e)

Solutions

  1. Read the stack trace embedded in the message to find the nil/invalid value; inspect the offending path's context in the plan state.
  2. Harden the goroutine body: nil-check context and context.Body before use, and validate planState maps.
  3. If plan state is corrupted, repair or re-load it via GetCurrentPlanState or restore from backup.
  4. Check for data races on planState/files/removed shared with the goroutines and add synchronization or copies.

Example fix

// before
_, hasPath := files[path]
// after (nil-safe)
if context == nil {
    errCh <- fmt.Errorf("nil context for path %s", path)
    return
}
_, hasPath := files[path]
Defensive patterns

Strategy: try-catch

Validate before calling

for path, ctx := range planState.ContextsByPath {
    if ctx == nil {
        return fmt.Errorf("nil context for path %q in plan state", path)
    }
}

Type guard

func isValidContext(c *shared.Context) bool {
    return c != nil
}

Try / catch

// already guarded in-library; at the call site:
out, err := GetPlanDiffs(orgId, planId, plain)
if err != nil && strings.HasPrefix(err.Error(), "error writing original files to temp dir: panic in GetPlanDiffs") {
    log.Printf("worker panic while diffing plan %s: %v", planId, err)
    // treat plan state as suspect: reload or surface to user
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/234463fb2e6c4e1b. Report an issue: GitHub.