skip to content

What results may a Go template.FuncMap helper return, and what happens when its error is non-nil?

level: middleimportance: should knowfreq 45%

answer

  1. one result, or one plus something
  2. the second result has a fixed type
  3. the action has nowhere to write if err != nil
  4. a non-nil error ends the render there
  5. Execute hands that error back, wrapped

basics

~20 s

A template function must return exactly one value, or two values whose second is of type error. Any other shape panics when you register it. If that error is non-nil during rendering, Execute stops there and returns the error to you, wrapped with the template name and position.

solid answer

~40 s

`template.FuncMap` entries are ordinary Go functions with one constraint on their results: either a single value, or two values of which the second is `error`. `Funcs` panics at registration for anything else, so the mistake never survives start-up. The second result is how a helper reports failure, because a template action has nowhere to put an error itself. When the helper returns a non-nil error during rendering, execution stops at that action and `Execute` returns the error, wrapped with the location — something like `template: invoice:3:12: executing "invoice" at <rate .Currency>: error calling rate: no rate for "XYZ"`. Because the wrapping uses `%w`, `errors.Is` and `errors.As` still work on the original error at the call site. The rule is the same for methods on the data value that a template invokes.

code

go · 9 lines
go
funcs := template.FuncMap{
	"rate": func(currency string) (float64, error) {
		r, ok := rates[currency]
		if !ok {
			return 0, fmt.Errorf("no rate for %q", currency)
		}
		return r, nil
	},
}

go deeper

for a junior

Memorise the shape: one return value, or two with the second being error. Know that returning a non-nil error from a helper makes the whole render fail rather than printing something empty.

for a middle

Explain why the second result exists — a template action has no room for an if err != nil — and describe what Execute returns: the helper's error wrapped with the template name, position and action text.

for a senior

Show judgment about which helper failures deserve to abort a render at all, and note that the error arrives after earlier output was already written. Mention that errors.Is still matches through the template's wrapper.

for a principal

Treat the error behaviour of each registered helper as part of the contract you publish to template authors: which helpers can stop a render, what the caller can match on, and what the fallback policy is.

## The contract Anything you put in a `template.FuncMap` is a plain Go function, with one rule about its results: - **one result** of any type, or - **two results**, the second of which has type `error`. A function with no results, three results, or two results whose second is not `error` is rejected — and rejected loudly: `Funcs` panics at registration rather than returning an error. That is the right trade, because it is a programmer mistake discovered at start-up, not a condition a running service can handle. The same rule applies to methods the template calls on the data. `{{.Balance}}` may resolve to a method `Balance() (int64, error)`, and it behaves exactly like a two-result func-map helper. ## Why the second result exists A template action is an expression, not a statement — there is no place in `{{rate .Currency}}` to write `if err != nil`. The two-result form is the escape hatch: the template package inspects the second result after every call, and if it is non-nil it aborts the render. Concretely, when the helper returns a non-nil error: 1. Execution stops immediately at that action. Nothing after it in the template runs. 2. `Execute` returns an error to the caller. 3. The error is wrapped with the template name, the line and column of the action, the text of the action, and the helper's name — for example `template: invoice:3:12: executing "invoice" at <rate .Currency>: error calling rate: no rate for "XYZ"`. 4. Because the underlying error is wrapped rather than stringified, `errors.Is` and `errors.As` at the call site still see your sentinel or typed error through the template's wrapper. That last point matters in practice: a helper can return a well-known sentinel and the handler that called `Execute` can distinguish "the customer has no configured currency" from "the template is broken" without parsing message text. ## One-result helpers and the panic hazard A one-result helper has no way to report failure, so it must not fail. If it panics — an index out of range, a nil map write, a type assertion — the panic propagates out of `Execute` and up through your handler. There is a partial safety net: the template packages recover panics raised inside called functions and turn them into an error return from `Execute` rather than letting them escape. Do not lean on it as a design. If a helper can fail on plausible input, give it the two-result form and return an error. ## Choosing between the two forms Use **one result** for total functions: formatting a number, upper-casing a string, computing a percentage from values that cannot be invalid. These read better in template source and cannot abort a render. Use **two results** when the helper does a lookup that can miss, parses something, or touches anything outside the process. Be deliberate about which failures deserve to kill the whole render: a missing translation is often better rendered as the key itself than as a failed page, while a missing tax rate on an invoice absolutely should stop the document. That decision has a cost you should name out loud: an error from a helper arrives **mid-render**, after earlier parts of the document have already been written. Deciding what a helper does on a miss is therefore also deciding whether a half-written document can reach the reader. ## Errors and the zero value When a two-result helper returns a non-nil error, its first result is ignored — the template does not print the zero value and carry on. There is no "soft" error. If you want a fallback, return it as the first result with a nil error and log inside the helper. ## A note on nil errors of a concrete type If a helper is declared `func(...) (string, error)` but returns a nil pointer of a concrete error type, the interface is non-nil and the render aborts on what you thought was success. Return a literal `nil` for the success path rather than a typed nil variable.

  • Does the first result still get printed when the helper's error is non-nil?
    No. There is no soft failure: a non-nil error discards the first result and aborts execution at that action. If you want a fallback rendered instead, return the fallback as the first result with a nil error and log the problem inside the helper — the two-result form is for failures you genuinely want to stop the render.
  • Can the caller of Execute recover the helper's original error with errors.Is?
    Yes. The template package wraps the helper's error with `%w` inside its positional message, so `errors.Is` and `errors.As` at the call site still match the sentinel or typed error the helper returned. That lets a handler distinguish "this customer has no configured rate" from "the template itself is broken" without matching on message text.
  • What happens if a one-result helper panics during rendering?
    The template packages recover panics raised inside functions they call and return them from `Execute` as an error rather than letting them unwind through your handler. It is a safety net, not a design: if a helper can fail on plausible input, declare it with the two-result form and return a real error so the failure is explicit and matchable.

saying these in an interview costs you the question

  • Thinks a helper may return any number of values
  • Expects the zero value to be printed when the error is non-nil
  • Believes a helper error is logged and rendering continues
  • Returns a typed nil error pointer on the success path
  • Assumes Execute discards the error's cause into a string