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
- Read the error. The parent of the named path is what is nil.
helm template --debugand readCOMPUTED VALUES.- Compare against your values file for a typo or different nesting.
--setcoerces types. Use--set-stringfor versions and tags.- Sensible fallback →
default. No fallback →requiredwith a real message. - Optional blocks →
with. Optional lists → guard withif, ordefault list. - Add
values.schema.jsonfor any chart other people install. - YAML parse errors → check
nindentagainstindent.
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.