In a Helm chart template, how do the `default` and `required` functions differ?
answer
- One continues the render, one stops it
- Fallback is the first argument, value arrives by pipe
- Sprig's idea of empty is wider than nil
- Zero and false are not the same to both
- Failure happens before any revision is recorded
basics
~20 sHelm's default function substitutes a fallback when a values entry is empty, so the render continues. required does the opposite: it aborts the whole render with the message you wrote. Use default for optional settings, required where no safe fallback exists.
solid answer
~50 sBoth decide what happens when a values entry is missing, and they are opposites. `{{ .Values.migration.backoffLimit | default 7 }}` returns 7 whenever the entry is *empty* — and sprig's idea of empty is broad: `nil`, `""`, `0`, `false` and empty collections all qualify, which is why `replicaCount: 0` silently renders 3 under a `default 3`. `{{ required "set migration.image" .Values.migration.image }}` returns the value untouched when it is set and fails the render with that message when it is nil or an empty string; a `false` or a `0` passes it. Because both run at render time, `helm template` and `helm lint` reproduce a failing `required` before anything reaches the cluster and before any release revision exists. Applying both to the same key is pointless — the default guarantees the required can never fire.
code
yaml · 12 linesapiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-migrate
spec:
backoffLimit: {{ .Values.migration.backoffLimit | default 7 }}
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: {{ required "migration.image is required: set it to the image holding the migration binary" .Values.migration.image | quote }}go deeper
Be ready to write both from memory: the fallback is the first argument to default so it pipes, and required takes a message then the value. Knowing which one stops the render is the whole first answer.
Explain the mechanics: sprig's emptiness test covers nil, empty string, 0, false and empty collections, while required only rejects nil and the empty string. Show the zero-and-false trap and the fix of declaring fallbacks in values.yaml.
Show judgement about where failures land. A required fails the render, so helm template in CI catches it before any revision exists; discuss which keys deserve required at all, and how the message doubles as the chart's error documentation.
Own the values contract: which entries the chart guarantees a working default for, which it refuses to guess, and how that decision is documented for consumers who never read your templates. Stacked defaults and hidden template-only fallbacks are the debt you are preventing.
### Two functions, opposite jobs A Helm chart is a set of Go templates rendered against a merged values tree, and every template eventually asks the same question: what do I do when the entry I need is not there? `default` and `required` are the two answers. `default` is a sprig function; `required` is one of the small set Helm adds on top of sprig. One keeps the render going with a fallback, the other stops it with a message you wrote. ### `default` It is written `{{ default 7 .Values.migration.backoffLimit }}` or, far more commonly, with the value arriving through a pipe: `{{ .Values.migration.backoffLimit | default 7 }}`. The fallback is the *first* argument precisely so the pipeline form reads naturally. The rule is that `default` returns the fallback when the value is **empty**, and sprig's notion of empty is wider than most people expect. All of these are empty: - `nil` (the key is absent entirely) - the empty string `""` - the number `0` - the boolean `false` - an empty list `[]` or empty map `{}` That breadth is the single most common bug written with this function. Take a batch-job chart that installs a database migration for a chat fan-out service. The template says `terminationGracePeriodSeconds: {{ .Values.migration.gracePeriod | default 45 }}`. An operator who wants the migration pod killed immediately sets `gracePeriod: 0` — and gets 45. The same trap bites booleans: `{{ .Values.metrics.enabled | default true }}` re-enables the thing the user just turned off, because `false` is empty. The fix is not a cleverer template. It is to put the fallback in `values.yaml`, where a consumer can read it, and template the value directly: `values.yaml` carries `gracePeriod: 45`, the template carries `{{ .Values.migration.gracePeriod }}`, and value merging does the work. Where you genuinely need an in-template presence test rather than an emptiness test, ask about presence explicitly — `hasKey .Values.migration "gracePeriod"`, or `kindIs "invalid"` to detect `nil`. Sprig's `coalesce` is the multi-argument cousin: it returns the first non-empty of several candidates, and inherits exactly the same definition of empty. ### `required` Written `{{ required "migration.image must be set to the fan-out service image" .Values.migration.image }}`. When the value is set, `required` returns it unchanged, so it composes in a pipeline: `{{ required "..." .Values.migration.image | quote }}`. When it is not, the render aborts and your message is what the operator sees. Its emptiness test is deliberately narrower than `default`'s: it rejects `nil`, and for strings it rejects `""`. A boolean `false` and a numeric `0` satisfy `required`. That asymmetry is worth remembering, because it means the two functions do not agree on what "missing" means, and a question about it separates people who have read the behaviour from people who have assumed it. Two consequences matter operationally. First, the failure is a **render** failure: `helm template`, `helm install --dry-run=client` and `helm lint` all reproduce it locally. Nothing is submitted to the cluster, no objects are applied and no release revision is recorded — the pipeline stops at the render stage. Second, `required` is only evaluated if the branch containing it is rendered. Wrapping it in `{{ if .Values.migration.enabled }}` makes the requirement conditional: the DSN is mandatory only when the pre-upgrade migration hook is actually part of the release. ### Writing them well The message is the entire user interface of `required`, so name the key path and say what to set: `"migration.image is required: set it to the image containing the migration binary"` beats `"image required"`. Do not stack them. `{{ required "..." (.Values.x | default "y") }}` can never fail, because the default has already made the value non-empty — it reads as a safety belt and is dead code. Do not hide defaults in templates. A `values.yaml` that omits a key while the template silently supplies `default 7` means a consumer reading the chart's values file cannot see the effective configuration, and `helm show values` — which prints `values.yaml` — tells them nothing about it. Declare the key with its fallback in `values.yaml`, and reserve `required` for the entries that genuinely have no sensible fallback: an image reference, a hostname, a connection string. Everything else should have a working default so that `helm install` with no flags produces something that runs.
- A user sets `replicaCount: 0` and the chart still renders 3 replicas. What happened, and how do you fix it?The template pipes the value through `default 3`, and sprig's `default` treats `0` as empty, so the fallback wins. The fix is to declare `replicaCount: 3` in `values.yaml` and template `{{ .Values.replicaCount }}` directly, so value merging supplies the fallback and an explicit `0` survives. If you must decide in the template, test presence with `hasKey` rather than emptiness.
- Does `required` fail when the value is `false`?No. `required` rejects `nil` and, for strings, the empty string; a boolean `false` or a numeric `0` passes straight through. So you cannot use `required` to insist that someone explicitly chose a boolean. If that is genuinely needed, check for the key's presence instead — `hasKey .Values "tls"` — and call `fail` with your own message when it is absent.
- Where does a failing `required` surface, and what state does it leave behind?At render time, so `helm template`, `helm install --dry-run=client` and `helm lint` all reproduce it on a laptop or in CI. Because the render never completes, no manifest is built, nothing is submitted to the API server, and no release revision is created — an existing release is untouched. That makes `required` a cheap gate: the pipeline fails before it can produce a half-applied upgrade.
default is a spare tyre in the boot; required is a warning light that refuses to let the car start. One lets the journey continue quietly, the other makes you fix the thing before you move.
saying these in an interview costs you the question
- Claiming default only fires when the key is absent
- Saying required also rejects false and 0
- Thinking a failing required creates a failed release revision
- Wrapping a default inside required as extra safety
- Keeping fallbacks only in templates, never in values.yaml
- Believing required is a sprig function like default