HelmHelm

Error: nil pointer evaluating interface {} in a Helm template

A template read a value that does not exist. How to find which path is missing, and write templates that fail with a useful message instead.

medium fix5 min read

the helm error
Error: INSTALLATION FAILED: template: api/templates/deployment.yaml:23:24: executing "api/templates/deployment.yaml" at <.Values.image.tag>: nil pointer evaluating interface {}.tag

Error: template: api/templates/ingress.yaml:8:14: executing "api/templates/ingress.yaml" at <.Values.ingress.hosts>: range can't iterate over <nil>

Error: YAML parse error on api/templates/configmap.yaml: error converting YAML to JSON

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Render locally and read the exact file, line and path

    helm template api ./chart --debug 2>&1 | head -30

    The error names the template, the line, and the value path it was evaluating. The parent of that path is what is nil, so .Values.image.tag failing means image itself is missing, not tag.

  2. 2

    See the values Helm actually computed

    helm template api ./chart -f values-prod.yaml --show-only templates/deployment.yaml --debug 2>&1 | sed -n '/COMPUTED VALUES/,/^$/p'

    --debug prints the merged values after all the files and --set flags are applied. Comparing this against what you expected usually shows a key nested one level differently than the template assumes.

  3. 3

    Make the template fail with a message a human can act on

    grep -rn "required\|default" chart/templates/ | head

    required gives a clear error naming the missing value, and default supplies a sensible fallback. Both are better than a nil pointer that names a line number in a file the user did not write.

All 8 sections

A Go template dereferenced something that is nil. Helm's error is precise if you read it carefully:

template: api/templates/deployment.yaml:23:24:
executing "api/templates/deployment.yaml" at <.Values.image.tag>:
nil pointer evaluating interface {}.tag

The parent is what is missing. interface {}.tag means Helm had something that was nil and tried to read .tag from it, so .Values.image does not exist. Not tag.

Look at the merged values

helm template api ./chart -f values-prod.yaml --debug 2>&1 \
  | sed -n '/COMPUTED VALUES/,/^$/p'

This is what Helm actually has after merging the chart's values.yaml, every -f file in order, and every --set. Comparing it against what you expected resolves most of these immediately.

Common discoveries:

A typo or a different nesting level.

# what the template reads
.Values.image.tag

# what values-prod.yaml has
imageTag: "1.4.2"

A -f file that was never loaded. A wrong path is not an error in every Helm version, so the file is silently ignored.

--set type coercion. --set image.tag=1.4 produces the float 1.4, not the string "1.4", which then renders as 1.4 and may not match a tag. Use --set-string image.tag=1.4.

Maps replace rather than merge for lists. A list in a values file replaces the chart's list entirely; it does not append. So overriding one element of ingress.hosts means supplying the whole list.

Fix 1: default

For anything with a sensible fallback:

image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"

default handles nil and also empty string, zero and false, which is occasionally not what you want. For a boolean that should be allowed to be false, test existence instead:

{{- if hasKey .Values "enableFoo" }}

Guard the parent when a whole block is optional:

{{- with .Values.resources }}
resources:
  {{- toYaml . | nindent 2 }}
{{- end }}

with skips the block entirely when the value is absent, and rebinds . to it inside, which is cleaner than repeating the full path.

Fix 2: required

For values with no sensible default, fail with a message the user can act on:

image: "{{ required "image.repository is required. Set it in values.yaml." .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
Error: execution error at (api/templates/deployment.yaml:23:14):
image.repository is required. Set it in values.yaml.

That is dramatically more useful than a nil pointer, especially for someone installing your chart who has never opened its templates.

required treats empty string as missing as well as nil.

Fix 3: Guard ranges

range can't iterate over <nil>
{{- range .Values.ingress.hosts }}

Either default to an empty list:

{{- range .Values.ingress.hosts | default list }}

Or guard the whole section:

{{- if .Values.ingress.enabled }}
{{- range .Values.ingress.hosts }}
...
{{- end }}
{{- end }}

The second is better, because a chart with ingress.enabled: false should not render an Ingress at all.

Declare the shape with a schema

values.schema.json in the chart root validates values before templates render, so users get a clear structural error rather than a nil pointer:

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["image"],
  "properties": {
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string" }
      }
    },
    "replicaCount": { "type": "integer", "minimum": 1 }
  }
}
Error: values don't meet the specifications of the schema(s) in the following chart(s):
api:
- image: repository is required

This is the most user-friendly option for a chart other people install, and it catches type errors such as a tag supplied as a number.

The YAML parse error variant

Error: YAML parse error on api/templates/configmap.yaml: error converting YAML to JSON

Usually indentation from a template function. nindent includes a leading newline and indent does not, and mixing them up breaks the document:

# correct
data:
  {{- toYaml .Values.config | nindent 2 }}

Render it and look:

helm template api ./chart --show-only templates/configmap.yaml

helm lint catches some of these, and rendering and reading the output catches more.

A checklist

  1. Read the error. The parent of the named path is what is nil.
  2. helm template --debug and read COMPUTED VALUES.
  3. Compare against your values file for a typo or different nesting.
  4. --set coerces types. Use --set-string for versions and tags.
  5. Sensible fallback → default. No fallback → required with a real message.
  6. Optional blocks → with. Optional lists → guard with if, or default list.
  7. Add values.schema.json for any chart other people install.
  8. YAML parse errors → check nindent against indent.

Frequently Asked Questions

Which value is actually missing?

The parent of the one named in the error. A message ending interface {}.tag while evaluating <.Values.image.tag> means Helm had a nil where image should be and tried to read tag from it, so image is the thing that does not exist. This reads backwards at first and saves a lot of time once you know it: people commonly go looking for a missing tag when the whole image block is absent.

What is the difference between default and required?

default supplies a fallback when a value is absent, so the chart installs without the user setting it. required fails the render with a message you write, so the user is told exactly what to set. Use default where there is a sensible value, such as falling back to .Chart.AppVersion for an image tag, and required where there genuinely is not, such as an image repository. Note that default also treats empty string, zero and false as missing, which occasionally surprises people with boolean flags.

Why did my --set value not work?

Most often type coercion. --set image.tag=1.4 produces the number 1.4 rather than the string "1.4", which renders differently and may not match a real tag. Use --set-string for anything that must stay a string. The other common cause is lists: a list supplied in a values file replaces the chart's list entirely rather than merging with it, so overriding one element of something like ingress.hosts means providing the whole list.

How do I give users a better error than a nil pointer?

Two mechanisms. required lets you attach a human-readable message to a specific value, which appears instead of the template error. And a values.schema.json in the chart root validates the entire values structure before any template renders, producing a structural error such as "image: repository is required". The schema is the better option for a chart other people install, because it also catches type mistakes and validates constraints like a minimum replica count.

Why do I get a YAML parse error instead of a nil pointer?

Because the template rendered successfully and produced invalid YAML, which is a different failure. The usual cause is indentation from a template function: nindent emits a leading newline and indent does not, so using the wrong one shifts a block and breaks the document structure. Render the single file with helm template --show-only templates/<file>.yaml and read the output. helm lint catches some of these, but reading the rendered YAML catches more.

Reference and practice

Learn the underlying concept

Other Helm errors