A critical behavior in TFPolicy is that the logical AND operator (&&) does NOT short-circuit in locals blocks, enforce {} blocks, or for...if predicates. Both sides of the && expression are always evaluated. If the left side is local.var != null and the right side is core::length(local.var), the policy will crash if local.var is null because the right side is still executed.
Correct Patterns:
- In
locals or enforce blocks: Use the ternary operator (? :) to guard function calls. Ternary operators do short-circuit. - In
for...if predicates: Use a ternary operator inside the predicate and ensure the second access is also wrapped in core::try() to prevent attribute-access errors.
# ❌ WRONG — condition = also does NOT short-circuit; crashes when local.X is null
enforce {
condition = local.X != null && core::contains(["a", "b"], local.X) # ❌ crashes!
}
# ✅ CORRECT — use ternary inside the locals block, then reference in condition
locals {
is_allowed = local.X != null ? core::contains(["a", "b"], local.X) : false
}
enforce {
condition = local.is_allowed
}
# ❌ WRONG — for...if predicate also does NOT short-circuit
violating = [
for r in local.rules : r
if core::try(r.field, null) != null && core::contains(["a", "b"], r.field) # ❌ crashes!
]
# ✅ CORRECT — use ternary in for...if predicate
violating = [
for r in local.rules : r
if (core::try(r.field, null) != null ? core::contains(["a", "b"], core::try(r.field, "")) : false)
]