plandex-ai/plandex · error

error starting fast apply: %v

Error message

error starting fast apply: %v

What it means

maybeStartFastApply's goroutine recovered from a panic (recover() != nil) and reports it as 'error starting fast apply'. It converts an unexpected runtime panic during the fast-apply race leg into a regular error sent to errCh, calls onFail to start the whole-file build, and calls runtime.Goexit to prevent double-sending on the channel.

Source

Thrown at app/server/model/plan/build_race.go:127

				sendRes(raceResult{content: content, valid: true})
			}
		}()
	}

	maybeStartFastApply := func(onFail func()) {
		log.Printf("buildRace - starting fast apply")
		if !params.didCallFastApply {
			log.Printf("buildRace - fast apply isn't defined, skipping")
			sendErr(nil) // no error, just no fast apply
			onFail()
			return
		}

		go func() {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("panic in maybeStartFastApply: %v\n%s", r, debug.Stack())
					sendErr(fmt.Errorf("error starting fast apply: %v", r))
					onFail()
					runtime.Goexit() // don't allow outer function to continue and double-send to channel
				}
			}()
			var fastApplyRes string

			select {
			case fastApplyRes = <-fastApplyCh:
			case <-buildCtx.Done():
				log.Printf("buildRace - context canceled, skipping fast apply")
				sendErr(nil) // no error, just no fast apply
				onFail()
				return
			}

			if fastApplyRes == "" {
				log.Printf("buildRace - fast apply isn't defined or failed to run")
				sendErr(nil) // no error, just no fast apply

View on GitHub (pinned to e2d772072e)

Solutions

  1. Read the debug.Stack() output logged as 'panic in maybeStartFastApply' to find the panicking frame
  2. Guard nil fileState/builderRun fields before entering the goroutine
  3. Add recover-safe wrappers around third-party syntax parsing calls
  4. Run with -race to detect data races between fast-apply and validation goroutines

Example fix

// before
fastApplyRes = <-fastApplyCh // panic if channel closed or state nil
// after
res, ok := <-fastApplyCh
if !ok || fileState == nil || fileState.builderRun == nil {
	sendErr(nil)
	onFail()
	return
}
fastApplyRes = res
Defensive patterns

Strategy: type-guard

Validate before calling

if fileState == nil || fileState.builderRun == nil {
	log.Printf("skipping fast apply: uninitialized state")
	sendErr(nil)
	onFail()
	return
}
if fastApplyCh == nil {
	sendErr(nil)
	onFail()
	return
}

Type guard

func fastApplyStateReady(fs *activeBuildStreamFileState, ch chan string) bool {
	return fs != nil && fs.builderRun != nil && ch != nil
}

Try / catch

go func() {
	defer func() {
		if r := recover(); r != nil {
			log.Printf("panic in maybeStartFastApply: %v\n%s", r, debug.Stack())
			sendErr(fmt.Errorf("error starting fast apply: %v", r))
			onFail()
			runtime.Goexit()
		}
	}()
	// fast-apply work
}()

Prevention

When it happens

Trigger: Any panic inside the fast-apply goroutine: e.g. nil map/slice access when reading fastApplyCh results, nil pointer on fileState.builderRun, index-out-of-range while processing the fast apply result, or a panicking library call before/around validateSyntax.

Common situations: Refactors that leave fileState fields nil when fast apply is invoked concurrently; third-party syntax-parsing libraries panicking on malformed input; data races writing shared state between the race goroutines.

Related errors


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