What a Terraform conditional actually evaluates, and where it breaks
A Terraform conditional is dangerous, not because condition ? true_val : false_val is hard to read, but because Terraform evaluates more of that expression than you might think, and treats a resource whose condition just flipped to false as gone, not moved.
A Terraform conditional is one line of syntax, which takes about thirty seconds: condition ? true_val : false_val, no if, no else, full stop.
Terraform's conditional expressions look like the ternary operator from any C-family language, but they're evaluated inside a language built to resolve a static plan from your configuration files before anything runs, a difference that produces failure modes a general-purpose if statement doesn't have.
- Two branches returning technically different types.
- Logical operators that don't short-circuit the way you'd bet money they would.
- A resource that doesn't shift position when its condition flips; it disappears entirely, taking every reference to it down with it.
This article covers those three mechanics in order, along with the count expressions, can() and try() functions, and precondition and postcondition blocks that take a conditional past the basic ternary.
It closes with what a flipped conditional means for everything that depends on the resource it just removed.
What is a Terraform conditional?
A Terraform conditional is a single expression that picks one of two values based on a boolean condition, written as condition ? true_val : false_val. If condition evaluates to true, the whole expression resolves to true_val. If it's false, it resolves to false_val. Terraform's configuration language has no if statement, no switch, and no block-scoped branching of any kind, because HCL describes a desired end state rather than a sequence of instructions to execute.
A Terraform conditional can appear almost anywhere Terraform expects a value, including local values, resource arguments, data sources, output values, and module call arguments.
It only ever chooses between values, though, never between entire resources or entire modules; picking whether an entire resource exists is a separate job, covered later under count.
The canonical use case replaces invalid values with a sensible default:
If var.environment comes in as an empty string, local.environment resolves to "development"; otherwise it passes the actual value through unchanged.
Terraform conditional syntax and how result types actually resolve
Both branches of a Terraform conditional must resolve to the same type, or to types Terraform can convert to a common one automatically.
Terraform converts numbers to strings implicitly, so var.enabled ? 12 : "hello" is valid HCL and returns a string in both branches, converting 12 to "12" behind the scenes. Relying on that conversion is a bad habit: it works until an input variable's actual type surprises you, and then the failure shows up as a confusing error elsewhere in the plan.
Opt for explicit conversion functions, tostring() or tonumber(), whenever there's ambiguity about what a branch should return.
Where the type rule actually bites is on collections, not scalars. Two branches returning different collection shapes, a map on one side and a list on the other, aren't compatible the way a number and a string are, and Terraform won't just reconcile them:
The fix is to make both branches the same collection type, even if one of them ends up empty:
An empty map costs nothing and keeps both branches structurally identical. Once both sides agree on shape, Terraform can determine the whole expression's type without knowing which branch will run, which is what it needs to build a plan before touching any real infrastructure.
Conditionals in Terraform: combining logic without short-circuiting
Terraform's logical operators, && (AND), || (OR), and ! (NOT), read like their equivalents in other languages, and combining multiple conditions with them is the normal way to build past a single boolean check:
What trips up engineers coming from almost any general-purpose language is that these operators don't short-circuit.
In a language with real control flow, false && anything_that_might_error() never evaluates the right side, because there's no reason to.
Terraform evaluates both operands regardless, since a Terraform conditional isn't branching execution the way an if statement does; it's resolving a single expression's value and type ahead of any actual run, which is exactly why something that looks completely safe can still fail.
If var.db_config is null, the intent of var.db_config != null && ... is obviously to stop before touching .engine_version on a null value.
Terraform doesn't stop there: both sides of the && get evaluated, and accessing an attribute on null produces an error regardless of which branch would ultimately win.
The fix that keeps the intent explicit is a nested conditional, checking for null first and only touching the attribute inside the branch where it's already known to be safe:
It works, though nesting conditionals two levels deep for a single default value is a lot of syntax for a small idea. try(), covered later in this article, does the same job with considerably less syntax.
Terraform conditional resource: creating and skipping resources with count
The most common reason anyone uses a Terraform conditional resource for is deciding whether to conditionally create resources at all, using count as the mechanism:
When var.enable_cache is true, count resolves to 1, and Terraform creates exactly one instance of the resource, addressable as aws_elasticache_cluster.session_cache[0].
When it's false, count resolves to 0, and the resource simply doesn't exist, neither in configuration output nor in state after the next apply.
This mechanic isn't the same as a count-driven list reordering and shifting every subsequent index (a real, separate failure mode with its own fix). A resource toggled by a boolean condition doesn't move when the condition flips; it vanishes entirely, and any indexed address that other resources use to reference it elsewhere becomes invalid, pointing at an index [0] that no longer exists:
The guard against this is the same conditional pattern applied a second time, at the point of reference rather than at the point of creation:
Every downstream reference to a conditionally created resource needs that same guard, independently, wherever it appears. Terraform won't infer it from the resource's own count argument.
Terraform count conditional beyond a simple toggle
A boolean toggle is the simplest Terraform count conditional pattern, but the count argument accepts any expression resolving to a whole number.
Combining a condition with the length function conditionally sizes a list of resources instead of just switching a single one on or off:
With enable_read_replicas set to false, no replicas exist, regardless of how many regions the list contains. Flip it to true, and Terraform creates one replica per entry in var.replica_regions, indexed in order; confirming exactly which indices ended up in state afterward is what you would use terraform state show for.
terraform.workspace is another useful input, since it turns a Terraform workspace selection into a condition, letting the same configuration behave differently across different environments without a separate input variable just to track which one is currently active:
The alarm only exists in the production workspace; every other workspace skips it. One caution: count and for_each are mutually exclusive on the same block, so a resource conditionally created with one can't also iterate with the other.
Safer Terraform conditional expressions: can, try, and collection checks
can() turns a whole expression into a boolean condition, returning true if it would evaluate without error and false if it wouldn't, which makes it a clean way to test whether something is safe to access before a Terraform conditional decides what to do with it.
Assigning the result to a local variable keeps the check reusable anywhere else in the module:
try() solves a closely related problem more directly: given a list of expressions, it returns the result of the first one that doesn't error. This tool replaces the nested conditional from earlier in this article:
If var.db_config is null, or exists without an engine_version attribute, try() moves on to the fallback instead of failing the way an unguarded && chain would.
The last technique worth having tests a condition across an entire collection at once, using a for expression together with alltrue() or anytrue():
local.all_types_approved is true only if every entry in var.instance_types is approved, which is a useful building block for a terraform conditional resource that should only proceed once an entire list of inputs passes some check, not just a single value.
Custom condition checks with preconditions and postconditions
Everything covered so far picks between two values. precondition and postcondition blocks do something different: they assert an invariant and stop the operation with a specific error message if it's violated.
A precondition, placed inside a resource's lifecycle block, gets checked while Terraform is still building its plan, before it tries to create or modify the resource. Adding one to a shortened version of the aws_db_instance.replica resource from earlier catches a real prerequisite before any replica gets created:
If aws_db_instance.primary.multi_az is false, Terraform stops before creating any replica and surfaces the error_message directly, instead of letting replica creation fail against the cloud provider's own, less specific error.
A postcondition checks a guarantee after Terraform has already created or read something, using the self object to reference the result's own attributes, confirming here that an AMI lookup returned the right CPU architecture:
Preconditions and postconditions require Terraform 1.2.0 or later. A precondition verifies an assumption before Terraform acts, whereas a postcondition verifies a guarantee about what it actually produced.
Neither replaces a Terraform conditional for picking a value, instead catching broken assumptions and providing a message that explains what went wrong.
Common mistakes to avoid making with Terraform conditionals
You already know that there is no if statement in HCL, no else, and no block-scoped branching, only the ternary expression itself.
Here is a subtler trap that shows up once a conditional gets long enough that splitting it across multiple lines feels natural:
Wrapping the expression in parentheses fixes it, and is worth doing by default on any conditional you expect to revisit:
The biggest mistake you can make, though, is the one covered above, referencing a conditionally created resource by its index without a matching guard at the point of reference. Writing the guard once at creation time is not enough.
It's easy to write the guard once, at creation time, and assume that's all you need to do, when every place downstream that touches resource[0] needs the same protection independently.
What a flipped conditional means for the rest of your graph
Everything above happens at the level of a single expression: one conditional, one resource, one output. None of it accounts for what else in your configuration depends on the resource a conditional just removed, which is another way of asking what the blast radius of that change actually is.
Terraform's dependency graph exists to answer "what depends on what," and it's what enables terraform plan to catch an invalid-index error the moment a downstream reference breaks.
However, that graph is built fresh for a single run and discarded once the run finishes; it tells you about the one reference that just broke, not everything already relying on the resource before that condition flipped.
Finding that out involves reading a plan's diff carefully to trace every consequence yourself, on a configuration where the resource might be referenced from three modules away.
That property is common to graphs that only exist for one invocation. It applies to any change that removes a resource; a conditional toggle is simply the most common way that removal gets noticed, since the whole point of count = condition ? 1 : 0 is making a resource's existence depend on something that can change.
Measuring the blast radius of a conditional before you apply it
Stategraph's Blast Radius Analysis may not alter how a terraform conditional is written or evaluated, as that's still entirely up to you and the HCL you write. What it does change is at what point you find out about the dependencies on the resource a conditional is about to remove.
Because Stategraph persists your dependency graph in a queryable form instead of rebuilding and discarding it on every run, you can ask what depends on a resource as a direct query rather than a manual trace through configuration:
You get every resource affected, along with its distance in the dependency chain: distance 1 means something references the resource directly and will definitely be affected; distance 2 or 3 means it's downstream of something that does.
Run that before flipping enable_cache to false, and you will know what else is about to lose a valid reference, before terraform plan reports the first one as an error and leaves you to guess whether there are more.
This visibility layer is not a substitute for the null guards and type-consistent branches covered earlier. A resource with zero real dependents gets a one-line answer. However, it comes into its own when encountering a conditionally created resource, referenced from more places than any one engineer remembers, about to have its condition flipped.
Conclusion
A Terraform conditional is one line of syntax that covers three mechanics: a type-resolution rule that punishes mismatched collection shapes, logical operators that evaluate both sides regardless of which one "wins," and a count-driven resource that disappears rather than shifts when its condition changes.
Past the ternary, precondition and postcondition blocks assert an invariant instead of just picking a value, and can()/try() handle the null-safety problem a nested conditional solves more awkwardly.
With the right tools, you find out earlier what a flipped condition is about to break.
Try Stategraph free and query the blast radius of your own conditionally created resources before the next terraform apply finds it for you.
Terraform conditional FAQs
Does Terraform have if/else statements?
No. HCL has no if, else, or other block-scoped branching, because a Terraform configuration describes a desired end state rather than executed instructions.
The ternary conditional expression, condition ? true_val : false_val, is the only conditional logic HCL supports directly, alongside count and for_each for controlling whether and how many times a block gets created.
Can you combine multiple conditions in a terraform conditional?
Yes, using && (AND), || (OR), and ! (NOT) inside the condition itself, for example var.enabled && var.environment == "production" ? 1 : 0.
These operators don't short-circuit, so var.x != null && var.x.attribute == "y" can still error on a null var.x instead of safely stopping at the first check.
What happens if the two result types in a terraform conditional don't match?
Terraform requires both branches to resolve to the same type, or to types it can convert automatically (numbers convert to strings, for instance).
Two branches returning incompatible collection shapes, such as a map on one side and a list on the other, fail during plan with an "Inconsistent conditional result types" error rather than silently picking one.
The fix is making both branches structurally identical, even if one side ends up an empty map or list.
How do you conditionally create a resource in Terraform?
Set the resource's count argument to a conditional expression that resolves to 1 or 0, for example count = var.enable_feature ? 1 : 0. When true, Terraform creates exactly one instance, addressable at index [0]. When false, the resource doesn't exist at all, and anything referencing it by index needs its own matching guard (typically ... : null) to avoid an invalid-index error once that index is gone.