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, and

View on GitHub (pinned to d32a084675)

Solutions

  1. Report it as a Terraform bug per the message text, including the command that produced it.
  2. If writing command code: always set op.PlanOutBackend (use the implied local backend when there is no backend block) before setting PlanOutPath.
  3. If migrating to the state-store API: set op.PlanOutStateStore instead.
  4. 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

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


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/7c0ad567040f798d. Report an issue: GitHub.