skip to content

Why must a Go template's Funcs call come before Parse when the template calls a custom function?

level: juniorimportance: must knowfreq 55%

answer

  1. two phases, not one
  2. the parser resolves names as it reads
  3. order of the chained calls matters
  4. New, then Funcs, then Parse
  5. "not defined" is a parse-time error

basics

~20 s

Parsing resolves every function name against the template's func map. Register the helper after Parse and the parser has never heard of it, so Parse fails with a "function not defined" error instead of failing later at Execute.

solid answer

~40 s

A Go template only knows the functions you gave it plus the builtins, and the parser checks that every name in the source exists **while it parses**. So the idiomatic order is `template.New(name).Funcs(fm).Parse(src)` — `New` returns a `*template.Template`, `Funcs` returns the same template so it chains, and `Parse` is last. `template.FuncMap` is a `map[string]any` from template-visible name to function value; `Funcs` panics right away if a key is not a valid identifier or the value is not a function with an acceptable result shape. Get the order wrong and `Parse` returns `template: invoice:1: function "money" not defined`. The nice side effect is that a typo in a helper name is caught at load time, not on the one request that happens to hit that branch.

code

go · 10 lines
go
tmpl, err := template.New("invoice").
	Funcs(template.FuncMap{
		"money": func(cents int64) string {
			return fmt.Sprintf("$%d.%02d", cents/100, cents%100)
		},
	}).
	Parse("Total: {{money .TotalCents}}")
if err != nil {
	return err
}

go deeper

for a junior

Be ready to write the chain from memory: create the template, attach the func map, then parse the source. Know that a helper the parser has not been told about makes Parse fail rather than Execute.

for a middle

Explain the two phases — the parser resolves names against the func map plus the builtins, and execution looks the function value up again. Mention that Funcs panics on a bad name or a bad function shape.

for a senior

Show where this sits in a service's lifecycle: parse once at start-up so a misspelled helper fails the deploy, keep the parsed template in a field, and never mutate the func map while requests are executing it.

for a principal

Frame the func map as an API you are handing template authors on another team. Every name you register is a promise about behaviour and error handling that you will be asked to keep across releases.

## What a func map is `text/template` and `html/template` both expose `template.FuncMap`, a `map[string]any` whose keys are the names the template source may call and whose values are ordinary Go functions. You attach one to a template with the `Funcs` method: ```go tmpl, err := template.New("invoice"). Funcs(template.FuncMap{"money": money}). Parse(src) ``` `Funcs` returns the same `*template.Template`, which is why the three calls chain. It may be called more than once; later calls merge into (and overwrite) earlier entries. ## Why the order is not a style preference Template loading has two distinct phases. `Parse` turns the source text into a parse tree, and while it does that it resolves every function name it meets. The parser looks the name up in two places: the template's own func map, and the fixed set of builtins (`index`, `printf`, `call`, `len`, `and`, `or`, `not`, `print`, `println`, `slice`, and the escapers). A name in neither place is a **parse error**, not a runtime one: ``` template: invoice:1: function "money" not defined ``` So if `Funcs` runs after `Parse`, the map exists but the parse already failed — nothing later can rescue it. However you load the source, whatever parses it has to be reached through a template that already carries the func map. This is a deliberate design choice, and a good one. Template sources are usually loaded at start-up, so a misspelled helper name blows up in your face during initialisation rather than on the one code path that renders it. Teams typically build the template once at start-up, keep the `*template.Template` in a struct field, and render from it per request. ## What `Funcs` validates, and when `Funcs` is not a passive assignment. It panics immediately if: - a key is not a valid Go identifier (`"my-helper"` is rejected — templates call functions by identifier, so use `myHelper`); - a value is not a function at all; - a function has an unusable result shape (it must return one value, or two of which the second is `error`). Because it panics rather than returning an error, this is registration-time programmer error, exactly like a bad regular-expression literal. ## Parse-time existence versus execute-time binding A subtlety worth knowing: the parser only checks that the *name* exists. The actual function value is looked up in the template's func map again at execution. So you can register a stand-in before `Parse` and replace the implementation with a second `Funcs` call before `Execute`, and the new implementation is the one that runs. That is how per-request helpers (a translator bound to the caller's locale, say) are sometimes wired — though re-registering on a shared template that other goroutines are executing is a data race, so most services register once and pass request state through the data value instead. ## What does not need registering Methods on the data do not go in the func map. If the value you pass to `Execute` has a method `Total()`, the template writes `{{.Total}}` and the template package finds it by reflection. The func map is for free functions that are not methods of the data — formatting, lookups, small transformations. Methods also follow the same one-or-two-results rule as func-map entries. ## Common failure modes - Building the template with `template.New("x")` and calling `Funcs` on a *different* template value than the one that parses the source. Only the template that parses sees the map. - Assuming a missing function is a runtime error you can recover from per request. It is not; the template never parsed. - Assuming builtins can be relied on for everything. The builtin set is small on purpose; anything domain-shaped — currency, dates in your house format, pluralisation — is yours to register.

  • Can you call Funcs a second time after Parse to swap a helper's implementation?
    Yes. The parser only checks at parse time that the *name* exists; the function value is looked up again in the template's func map during execution, so a later `Funcs` call with the same key wins. It is genuinely useful for stubbing a helper in tests, but mutating a template that other goroutines are executing is a data race — most services register once at start-up and pass per-request state through the data value instead.
  • Do methods on the data value need to be registered in the FuncMap?
    No. `{{.Total}}` will call a method `Total()` on the data by reflection with no registration at all, and the same one-or-two-results rule applies to it. The func map is only for free functions that are not methods of the value you pass to `Execute` — formatting helpers, lookups, small transforms.
  • What does Funcs do if you register something it cannot use?
    It panics on the spot, at registration. A key that is not a valid Go identifier (for example `"format-money"`), a value that is not a function, or a function with an unusable number of results are all rejected immediately rather than reported as an error return. Treat it like a bad literal: it is a programmer bug that should fail at start-up.

saying these in an interview costs you the question

  • Thinks unknown function names are only caught at Execute
  • Calls Funcs on a different template than the one that parses
  • Believes FuncMap keys may contain hyphens or dots
  • Expects a missing helper to render as empty output
  • Thinks methods on the data must be registered too