Terraform try

Terraform HCL Functions Infrastructure as Code

What is Terraform try?

The Terraform try function takes a series of expressions as arguments and returns the result of the first one that evaluates without error. If the first expression fails (say, because it references an object attribute that doesn't exist at runtime), Terraform moves to the next expression, and the next, until one succeeds or the list runs out.

Basic Terraform try syntax

The general form of the function looks like this:

try(expression_1, expression_2, ..., expression_n)

Terraform works through each of the argument expressions in order and returns the result of the first successful evaluation (try shipped in Terraform 0.12.20, released in January 2020, so any version still in active use today has it).

You might, for example, reach into a map for a key that may or may not be present, with a plain string as the fallback value.

locals {
region_tag = try(var.tags["region"], "unspecified")
}

If var.tags contains a region key, local.region_tag becomes that value. If the key doesn't exist, the reference would normally fail and halt the plan, but try catches that failure and falls back to "unspecified" instead. Per the Terraform documentation, this is exactly the intended use: concise testing for the presence of an attribute, rather than writing conditional logic every time.

What Terraform try actually does

try catches and handles dynamic errors resulting from data that isn't known until Terraform actually runs, rather than validating configuration before that point. A typo in a resource reference or a fundamentally broken expression won't be caught by try, because those are static problems that exist regardless of what data shows up at runtime.

There's a related edge case worth knowing before you lean on try for anything that might legitimately be empty: a successful evaluation that happens to produce null still counts as success.

Terraform never had to catch an error to get there, so try(null, "fallback") returns null instead of the fallback. Pair try with coalesce when you need a null result treated the same way as a missing one.

If try works through every expression you've given it and none of them succeed, it returns an error describing all of the failed attempts, not just the last one. That's intentional: you get visibility into every failure along the chain, which is valuable when a normalization local has three or four fallback expressions, and you need to know exactly where each one broke.

We recommend that you keep try narrow: simple attribute references and type conversions, not chains of unrelated logic.

A better idea, in general, is to confine try to special local values with expressions that perform normalization, so error handling stays in a single location, and the rest of the module can rely on straightforward references to that normalized structure. Future maintainers get something readable instead of error handling scattered through different resource blocks.

An example of Terraform try in action

Use try in a module when you genuinely don't control the shape of the data you're working with.

Optional object attributes are the clearest example. If a variable is typed as an object but one of its fields isn't guaranteed to be set by every caller of the module, try lets you fall back instead of forcing every caller to supply every field, sometimes across multiple levels of nesting.

You can also normalize values that could arrive in two different forms. A variable might reasonably be passed as a single string in one module call and a list of strings in another, and rather than pushing that inconsistency downstream into every resource that references it, you can use try to attempt each form in turn and land on one normalized structure that the rest of the module treats consistently.

Another use case is data decoded from external JSON or YAML. Once you pull in a file with jsondecode or yamldecode, or read the response from a data source, Terraform has no static guarantee which keys will be present in the result.

Say a module reads a shared team config file that different teams have filled in inconsistently:

locals {
team_config = jsondecode(file("${path.module}/team-config.json"))
alert_channel = try(local.team_config.notifications.slack_channel, "#platform-alerts")
escalation_min = try(tonumber(local.team_config.escalation_minutes), 15)
}

If a given team's team-config.json omits the notifications block entirely, or sets escalation_minutes as a string instead of a number, the attribute reference or the type conversion fails, try catches it, and local.alert_channel and local.escalation_min still resolve to workable defaults instead of halting the plan for every team that hasn't filled in every field.

Wrapping specific attribute references in try, with a sensible default, enables you to handle the edge cases that can appear when a plan depends on data Terraform doesn't fully control.

Common mistakes users make with Terraform try

try is narrower than it first appears, and most of the confusion on the HashiCorp community forums traces back to that gap between what it looks like it should catch and what it actually catches.

The most common mistake isn't really about try at all: it's writing ${...} string-interpolation syntax inside an expression argument that's already being evaluated as HCL.

One user's main.tf broke with a parse error on a line shaped like try(local.ep_urls[${var.region_short}][${var.env}], null), and the fix had nothing to do with try's fallback behavior: dropping the extra ${} braces resolved it, because local.ep_urls[var.region_short][var.env] is a valid expression on its own once you're inside an argument position rather than a string.

As covered above, try also won't catch errors relating to a malformed resource reference or any other expression that's provably invalid, regardless of what data you feed it. If your HCL has a genuine mistake in it, try won't hide that from you, and that's by design; only dynamic, data-dependent failures qualify.

If you find yourself nesting try calls or stacking several fallback expressions to handle unrelated failure modes, that's usually a sign the logic belongs somewhere more explicit.

There's a known quirk with boolean values that could make you hesitate to rely on try for anything true or false.

Some users have found that an expression that technically accepts and returns true or false doesn't always behave the way they'd expect inside try, and the HashiCorp community has an active discussion thread on the specifics – worth a read if boolean handling is central to what you're building.

In that thread, the root cause traces back to the same null-is-success behavior covered above: when an object attribute is itself declared with optional(bool), an omitted value resolves to null rather than throwing, so try's fallback never gets a chance to fire.

A subtler issue is a habit that you can pick up rather than a Terraform error. Newer Terraform users sometimes reach for try the moment something breaks, using it to suppress errors and mask a genuine configuration mistake rather than to handle data that's legitimately uncertain.

A missing resource reference or an incorrectly named variable should produce an error you fix, not an error you wrap in try and forget about. Save try for cases where the uncertainty is real and expected, not as a blanket answer to anything that fails.

The nearest alternatives to Terraform try: try vs. lookup

Try and lookup both handle missing data, but they solve different problems, and picking the wrong one tends to produce code that's harder to read than either function would be on its own.

  • The lookup function retrieves a value from a map by key, with an optional default returned if that key isn't present. It does one thing, in a single, readable expression.
  • try is a more general form of error handling. It evaluates a list of expressions and returns whichever one succeeds first, which makes it more flexible but also easier to misuse.

Use lookup for straightforward map key retrieval where you want a fallback value if the key is absent. Use try when you're checking for the existence of nested or uncertain attributes, or normalizing mixed types into one predictable shape.

try is the more flexible of the two, but that flexibility is exactly why overusing it can obscure genuine errors and make a module harder for the next engineer to get to grips with.

Scenario Better fit
Retrieving a value from a flat map by key lookup
Checking whether a nested or uncertain attribute exists try
Normalizing a value that might arrive in different types try
A map access where you already know the key structure lookup

try and can are close cousins (they shipped together and share the same dynamic-error-catching mechanism), but they answer different questions.

  • try returns a value
  • can returns a boolean

Use can inside a variable's validation block, where Terraform expects a true or false condition rather than a fallback value, and use try everywhere you actually need the successfully evaluated result to flow into the rest of the configuration.

coalesce covers a narrower case than try does: it assumes the expression itself evaluates without error and picks the first argument that isn't null or an empty string.

try handles the harder scenario, where the expression might fail to evaluate at all (an attribute that doesn't exist, a conversion that can't succeed). coalesce(var.example, "default") is the more direct tool when you already know var.example exists but might be null.

try is what you actually need when var.example.nested_field might not exist as a path at all, so nesting the two together as coalesce(try(local.value, null), "fallback") covers both cases – an expression that might fail and a result that might come back null – in one line.

For optional object-typed variables specifically, optional() in the variable's type constraint is often the better fit than reaching for try on every access.

Declaring retention_days = optional(number, 30) as part of the type (available since Terraform 1.3) tells Terraform upfront that the field might be absent and what to use instead, which is more explicit than catching the failure after the fact every time the field gets referenced.

try is still useful for data outside Terraform's influence, like decoded JSON or a data source's response, where there's no type constraint to declare an attribute optional against in the first place.

Stategraph's perspective on Terraform try

Functions like try exist so that your configuration handles uncertain data without falling over. That's one layer of resilience. The other layer, which is important once you're running Terraform at any real scale, is how reliably your state itself is stored, tracked, and governed.

Stategraph doesn't change how try, lookup, coalesce, or any other Terraform function evaluates or behaves. It isn't an alternative to Terraform; it's a state backend built for teams that need more visibility and control over what's actually happening to their state files.

For teams managing complex, multi-environment Terraform state, Stategraph provides audit history and compliance-relevant tracking of changes, plus drift detection that flags when real infrastructure has diverged from what your code declares, so you know not just that something changed but who changed it, when, and whether it still matches reality.

Writing defensive, resilient configuration and having a place to store the results of that configuration are separate challenges, but both need solving. Try Stategraph free to see how it fits into the rest of your Terraform workflow.

Related Terraform terms

  • Terraform map variables: try most often shows up wrapped around a map index, exactly the pattern this guide covers in depth.
  • Terraform regex function: pairing regex with try is the standard way to fall back when a pattern doesn't match.
  • Configuration drift: the operational risk that shows up when a try fallback silently changes what actually gets applied.
  • Blast radius in Terraform: understanding how far a fallback-driven change can spread through dependent resources.