hashicorp/terraform · error

invalid expression for variable %q: %s

Error message

invalid expression for variable %q: %s

What it means

Thrown by `decode_tfvars` (functions.go:134) when a top-level attribute parses but its expression cannot be evaluated with a nil EvalContext. Because `decode_tfvars` evaluates each attribute with no evaluation context, any function call, variable reference, or unknown symbol makes `attr.Expr.Value(nil)` fail, and the attribute name plus the HCL diagnostic are returned.

Source

Thrown at internal/builtin/providers/terraform/functions.go:134

	// stuff HCL diagnostics into plain string error messages. This produces
	// a non-ideal result but is still better than hiding the HCL-provided
	// diagnosis altogether.
	f, hclDiags := hclsyntax.ParseConfig(src, "<decode_tfvars argument>", hcl.InitialPos)
	if hclDiags.HasErrors() {
		return cty.NilVal, fmt.Errorf("invalid tfvars syntax: %s", hclDiags.Error())
	}
	attrs, hclDiags := f.Body.JustAttributes()
	if hclDiags.HasErrors() {
		return cty.NilVal, fmt.Errorf("invalid tfvars content: %s", hclDiags.Error())
	}
	retAttrs := make(map[string]cty.Value, len(attrs))
	for name, attr := range attrs {
		// Evaluating the expression with no EvalContext achieves the same
		// interpretation as Terraform CLI makes of .tfvars files, rejecting
		// any function calls or references to symbols.
		v, hclDiags := attr.Expr.Value(nil)
		if hclDiags.HasErrors() {
			return cty.NilVal, fmt.Errorf("invalid expression for variable %q: %s", name, hclDiags.Error())
		}
		retAttrs[name] = v
	}

	return cty.ObjectVal(retAttrs), nil
}

func encodeExprFunc(args []cty.Value) (cty.Value, error) {
	// These error checks should not be hit in practice because the language
	// runtime should check them before calling, so this is just for robustness
	// and completeness.
	if len(args) > 1 {
		return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected")
	}
	if len(args) == 0 {
		return cty.NilVal, fmt.Errorf("exactly one argument is required")
	}

View on GitHub (pinned to c9def3e214)

Solutions

  1. Replace every function call or reference with its already-computed literal value before passing the string to `decode_tfvars`.
  2. If you need evaluated tfvars, load the file normally via the root module's variable declarations instead of through `decode_tfvars`.
  3. Use `encode_tfvars`/`encode_expr` to produce literal-only strings you can safely round-trip back.
  4. Scan the source for `(` , `[a-z].*\.` references in a pre-check (see defense) to fail fast.

Example fix

// before
decode_tfvars("region = upper(var.region)")

// after
decode_tfvars("region = "US-EAST-1"")
Defensive patterns

Strategy: try-catch

Validate before calling

// Heuristic pre-check: reject obvious function calls and references.
var exprish = regexp.MustCompile(`(?m)^\s*[A-Za-z_][\w.-]*\s*=\s*[A-Za-z_][\w]*\s*\(`)
func hasLikelyCallOrRef(src string) bool { return exprish.MatchString(src) }

Try / catch

val, err := provider.CallFunction(providers.CallFunctionRequest{
    FunctionName: "decode_tfvars",
    Arguments:    []cty.Value{cty.StringVal(raw)},
})
if err != nil {
    // attribute-level parse failure; surface err to the user verbatim,
    // it already names the offending variable.
    return fmt.Errorf("decode_tfvars rejected input: %w", err)
}

Prevention

When it happens

Trigger: The tfvars string contains a function call such as `name = upper("x")`, a reference such as `name = var.region`, or any identifier that is not a literal. These are legal in normal `.tfvars` evaluation against a configured root module but are rejected here because `decode_tfvars` is a pure literal parser.

Common situations: Authoring tfvars that rely on interpolation and expecting `decode_tfvars` to resolve them; round-tripping an object that included function calls; feeding tfvars produced by tooling that emits references.

Related errors


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