hashicorp/terraform · critical
PlanOutPath set without also setting PlanOutStateStore or Pl
Error message
PlanOutPath set without also setting PlanOutStateStore or PlanOutBackend (this is a bug in Terraform)
What it means
Returned by Local.opPlan when op.PlanOutPath is non-empty but neither op.PlanOutStateStore nor op.PlanOutBackend is set. A plan file must embed a description of where state lives so a later apply can reconcile it; the message explicitly states this is a bug in Terraform because the command layer should always populate one of the two.
Source
Thrown at internal/backend/local/backend_plan.go:163
return
}
// Record whether this plan includes any side-effects that could be applied.
runningOp.PlanEmpty = !plan.Applyable
// Save the plan to disk
if path := op.PlanOutPath; path != "" {
switch {
case op.PlanOutStateStore != nil:
plan.StateStore = op.PlanOutStateStore
case op.PlanOutBackend != nil:
plan.Backend = op.PlanOutBackend
default:
// This is always a bug in the operation caller; it's not valid
// to set PlanOutPath without also setting PlanOutStateStore or PlanOutBackend.
// Even when there is no state_store or backend block in the configuration, there should be a PlanOutBackend
// describing the implied local backend.
diags = diags.Append(fmt.Errorf(
"PlanOutPath set without also setting PlanOutStateStore or PlanOutBackend (this is a bug in Terraform)"),
)
op.ReportResult(runningOp, diags)
return
}
// We may have updated the state in the refresh step above, but we
// will freeze that updated state in the plan file for now and
// only write it if this plan is subsequently applied.
plannedStateFile := statemgr.PlannedStateUpdate(opState, plan.PriorState)
// We also include a file containing the state as it existed before
// we took any action at all, but this one isn't intended to ever
// be saved to the backend (an equivalent snapshot should already be
// there) and so we just use a stub state file header in this case.
// NOTE: This won't be exactly identical to the latest state snapshot
// in the backend because it's still been subject to state upgrading
// to make it consumable by the current Terraform version, andView on GitHub (pinned to d32a084675)
Solutions
- Report it as a Terraform bug per the message text, including the command that produced it.
- If writing command code: always set op.PlanOutBackend (use the implied local backend when there is no backend block) before setting PlanOutPath.
- If migrating to the state-store API: set op.PlanOutStateStore instead.
- In tests, populate PlanOutBackend with a stub backend to exercise the plan-write path.
Example fix
// before: op set PlanOutPath but no backend/store op.PlanOutPath = "tfplan" // after: also record the (implied) backend so the plan is self-describing op.PlanOutPath = "tfplan" op.PlanOutBackend = impliedBackendForConfig(cfg)
Defensive patterns
Strategy: validation
Validate before calling
// Command-layer invariant check before Operation is called.
func validatePlanOut(op *backendrun.Operation) error {
if op.PlanOutPath != "" && op.PlanOutStateStore == nil && op.PlanOutBackend == nil {
return fmt.Errorf("internal: PlanOutPath set without PlanOutBackend/PlanOutStateStore")
}
return nil
} Type guard
null
Try / catch
rop, err := b.Operation(ctx, op)
if err != nil && strings.Contains(err.Error(), "PlanOutPath set without") {
// this is a Terraform bug; collect op dumps and report
return err
} Prevention
- In command code, always set PlanOutBackend (even if it is just the implied local backend) whenever PlanOutPath is set.
- Add a test that constructs an Operation with PlanOutPath set and asserts one of PlanOutBackend/PlanOutStateStore is also set.
- Treat this message as a development-time assertion, not a user-facing condition.
When it happens
Trigger: Reaching the default branch of the switch at backend_plan.go:153. The CLI/command code that builds the Operation set PlanOutPath but forgot to set PlanOutStateStore (new state-store path) or PlanOutBackend (legacy/implied-local backend). In shipped Terraform this path is unreachable; it appears only with a regression or a custom command.
Common situations: Developing a new Terraform command or plan-output path without wiring the backend/store field; a fork that constructs backendrun.Operation manually; running an in-tree test that forgets to populate the backend on the Operation.
Related errors
- unsupported operation type: %s This is a bug in Terraform a
- error: using a saved cloud plan when executing Terraform loc
- backendDiags.Err()
- unknown view type %v
- operation timed out
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/7c0ad567040f798d.
Report an issue: GitHub.