How to Fix Terraform 'Error: Invalid for_each Set Value' Caused by Null in a Set
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: Terraform's
for_eachreceived asetcontaining at least onenullelement — this is a hard runtime error; Terraform refuses to proceed and the entireapplyorplanaborts. - How to fix it: Filter nulls out of the collection before it reaches
for_eachusingcompact(), aforexpression with anifguard, ortoset([ for v in var.items : v if v != null ]). - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your failing
.tfblock and get corrected HCL without sending your state or secrets anywhere.
The Incident (What Does the Error Mean?)
You hit this during terraform plan or terraform apply:
Error: Invalid for_each set value
on main.tf line 12, in resource "aws_iam_role_policy_attachment" "this":
12: for_each = toset(var.policy_arns)
Invalid single element for a set: element index 2 is null.
Only non-null values may be used as set element keys.
Immediate consequence: The entire Terraform operation halts. No resources in the run are created, updated, or destroyed — including unrelated resources later in the graph. In a pipeline, this breaks the deployment stage completely. If this is a partial re-apply after a failed run, your state may already have diverged.
The Attack Vector / Blast Radius
This is not just a syntax annoyance. The blast radius is:
- Pipeline hard-stop. Every downstream job depending on Terraform outputs (
terraform output) gets null or stale values. Services expecting freshly provisioned ARNs, IPs, or IDs break silently or loudly depending on how your pipeline handles exit codes. - State drift. If this apply was mid-flight (e.g., some resources already created in a prior partial run), you now have real infrastructure with no corresponding state lock. Drift accumulates.
- Root cause is almost always dynamic input. The null sneaks in from: an optional variable with no default, a
lookup()that missed a key, adatasource attribute that returned null because the resource doesn't exist yet, or a list built fromjsondecode()on a sparse JSON blob. The set type in Terraform does not tolerate nulls — ever. toset()does not sanitize. Many engineers assumetoset()will coerce or drop nulls. It does not. It hard-errors.
How to Fix It (The Solution)
Basic Fix — Filter Nulls Before toset()
The minimal surgical fix: use a for expression with a null guard.
resource "aws_iam_role_policy_attachment" "this" {
- for_each = toset(var.policy_arns)
+ for_each = toset([for arn in var.policy_arns : arn if arn != null])
role = aws_iam_role.this.name
policy_arn = each.value
}
For list(string) inputs that may contain empty strings as well as nulls, use compact() (drops both nulls and empty strings):
resource "aws_iam_role_policy_attachment" "this" {
- for_each = toset(var.policy_arns)
+ for_each = toset(compact(var.policy_arns))
role = aws_iam_role.this.name
policy_arn = each.value
}
⚠️
compact()only works onlist(string). If your set contains objects or complex types, use theforexpression withif v != null.
Enterprise Best Practice — Validate at the Variable Boundary
Don't let nulls propagate into your module internals. Enforce non-null at the variable declaration using validation blocks, and use a nullable-safe default.
variable "policy_arns" {
- type = list(string)
- default = []
+ type = list(string)
+ default = []
+
+ validation {
+ condition = alltrue([for arn in var.policy_arns : arn != null && arn != ""])
+ error_message = "policy_arns must not contain null or empty string values."
+ }
}
For module authors publishing reusable modules, always defensively sanitize inside the module even if you validate at the boundary — consumers will pass bad data:
locals {
- policy_set = toset(var.policy_arns)
+ policy_set = toset([for arn in var.policy_arns : arn if arn != null && arn != ""])
}
resource "aws_iam_role_policy_attachment" "this" {
for_each = local.policy_set
role = aws_iam_role.this.name
policy_arn = each.value
}
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
Stop this class of error before it ever reaches plan.
1. terraform validate in pre-commit
Add terraform validate as a pre-commit hook. It catches type mismatches early, though it won't catch runtime-null issues from dynamic data sources.
2. Checkov — static analysis for Terraform Checkov won't catch this specific null issue natively, but custom checks can be written:
checkov -d . --check CKV_TF_FOR_EACH_NULL
Write a custom Checkov Python check that parses HCL AST and flags any toset(var.*) call without a null filter.
3. OPA/Conftest policy
Enforce in your pipeline with a Rego policy that rejects plans where for_each expressions reference raw variable sets without a compact or null-filter expression:
terraform show -json tfplan.binary | conftest test - --policy ./policies/foreach_null.rego
4. tflint
Use tflint with the terraform ruleset. Add a custom rule to flag toset(var.*) patterns:
tflint --enable-rule=terraform_required_version
5. Strongly-typed variable defaults
Never declare list(string) variables without a non-null default and a validation block. Make nulls impossible at the input layer, not just at the resource layer.
variable "policy_arns" {
type = list(string)
default = []
nullable = false # Terraform 1.1+ — prevents null being passed as the entire variable
}
nullable = false (Terraform ≥ 1.1) prevents a caller from passing null as the variable value itself — it falls back to the default. This does not prevent null elements inside the list, which is why the for expression guard is still required.