skip to content

Why does an html/template escaping error surface from Execute rather than from Parse?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Parse only builds a tree
  2. a second walk decides positions
  3. it has to follow template calls first
  4. runs the first time you render
  5. a page nobody renders is unchecked

basics

~20 s

Parse only builds the parse tree. html/template's escaping pass is a second walk that needs the whole template set, so it runs lazily on the first Execute or ExecuteTemplate — which means a template nobody renders is never checked.

solid answer

~40 s

`html/template` splits the work. `Parse` delegates to the same parser `text/template` uses and produces a tree; no output position is decided yet. The escaping pass runs later, at the first `Execute` or `ExecuteTemplate`, because it has to follow `{{template}}` calls and therefore needs every associated template present. That pass can fail — a template ending inside an attribute value has no output position, `{{if}}` branches ending in different positions are ambiguous — and the failure comes back as an error from `Execute`, typed `*template.Error` with a name, line and description. Once escaped, the template is frozen: `Parse` on it afterwards returns an error. The operational consequence matters most: render every template once in a test or at startup, or a page nobody exercised fails the first time a user reaches it.

code

go · 5 lines
go
// the opening quote on title is never closed
tmpl := template.Must(template.New("p").Parse(`<a title="{{.T}}>click</a>`))

err := tmpl.Execute(os.Stdout, data)
// err is non-nil: the template ends in a non-text context

go deeper

for a junior

Know that a template can parse successfully and still fail when it is rendered, so rendering it at least once in a test matters more than it looks.

for a middle

Explain the two passes: Parse builds the tree, and the escaping pass runs at the first Execute because it must follow template calls to know each action's output position.

for a senior

Show the operational consequence — an unexercised page fails the day it is first served — and the fix: execute every template in a smoke test or at startup, and log the *template.Error's name and line.

for a principal

Decide where template validation belongs in the pipeline. Rendering every template with a zero model at build time turns a rare production 500 on an error page into a failing build, and that is a cheap policy to standardise.

## Two passes, two moments `html/template` is `text/template` plus an escaping pass, and the two run at different times. `Parse` (and `ParseFiles`, `ParseFS`, `ParseGlob`) does lexing and parsing only. It produces a tree of nodes — text, actions, `if`, `range`, `with`, `template` calls — and reports syntax errors: an unclosed `{{`, an unknown function name, a malformed pipeline. It does not look at the HTML at all. The escaping pass walks that tree, parses the literal text as markup, computes the output position at each action, and rewrites the pipelines to insert escapers. It runs on the first `Execute` or `ExecuteTemplate` of a template, and it must be lazy: an action may be `{{template "row" .}}`, and to know what position `row`'s own actions occupy the pass has to follow the call. Templates are added to a set incrementally, so the earliest moment at which the whole set is known to be complete is the moment you ask for output. ## What can only fail in the second pass **A template that ends in a non-text position.** `<a title="{{.T}}>click</a>` — the quote is never closed, so at the end of the template the output is still inside an attribute value. There is no valid context, and the pass reports it. **Ambiguity across branches.** If one branch of an `{{if}}` leaves the output inside a URL and the other leaves it in ordinary text, a following action has two possible positions and cannot be escaped correctly for both. This is `ErrAmbigContext` in the package's error codes. **A shared template used from incompatible positions.** A named template called once from body text and once from inside an attribute would need two different escapings of the same tree; the pass cannot compute one output position (`ErrOutputContext`). **Dynamic markup.** `<{{.Tag}}>` or `<a {{.Attrs}}>` give the pass nothing analysable. **A conflicting manual escaper.** Applying a predefined escaper such as `html` or `urlquery` by hand in a pipeline can conflict with what the pass would do, and is reported rather than silently allowed. All of these arrive as an error return from `Execute`. The concrete type is `*template.Error`, which carries an `ErrorCode`, the template `Name`, the `Line` and a `Description` — worth unwrapping and logging rather than printing bare, because the line number is what makes a large template set tractable. ## What is not an error A value rejected at runtime is a different thing entirely. If a URL fails the scheme filter, the output contains `#ZgotmplZ` and `Execute` returns nil. Nothing is logged, nothing fails. That marker was chosen to be easy to grep for exactly because it is otherwise silent — if you have never searched your rendered output or your integration-test fixtures for it, you do not know whether it is there. ## Two operational consequences **The template is frozen after escaping.** Because the pass rewrites the tree in place, you cannot add to a template after it has been executed; `Parse` on it afterwards returns an error. Parse everything you need at startup, and if you need per-request variation, `Clone` before you diverge. **An unexercised template is an unchecked template.** This is the part that bites in production. The happy-path page is rendered constantly, so its escaping was validated on the first request after deploy. The error page, the admin-only page, the email preview and the branch behind a feature flag may not be rendered for weeks — and when they are, the first render is the one that runs the escaping pass and returns the error, inside a handler, to a user. The defence is cheap and mechanical: after parsing, execute every template once. In a test, iterate the set and render each template into `io.Discard` with a zero-value model, failing on any error; or do it at startup so the process refuses to come up rather than serving a broken page later. Either way the failure moves from a rare 500 to a red build or a failed deploy, which is where a template bug should live. ## What an interviewer is listening for The split between parsing and escaping, the reason the second pass is lazy (it needs the whole set), at least one concrete failure that only the second pass can find, the distinction from the silent `ZgotmplZ` substitution, and the habit of rendering every template in a test so the laziness cannot hurt you.

  • Why can't the escaping pass run during Parse?
    Because it follows `{{template}}` calls to work out what position the called template's actions occupy, and templates are added to a set one call at a time. At any given Parse the set may still be incomplete, so the earliest safe moment is when output is requested.
  • What happens if you call Parse on an html/template that has already been executed?
    It returns an error. The escaping pass rewrote the tree in place, so adding to the set afterwards would leave new content unescaped or invalidate the analysis. Parse everything at startup, and use Clone when a variant needs its own function map or extra definitions.
  • You see #ZgotmplZ on a page. Did Execute return an error?
    No. That marker is a runtime substitution by the URL or CSS filter for a value it will not emit; rendering succeeds and nothing is logged. Treat it as a data problem — usually an unexpected scheme or an already-built URL handed in whole — and grep rendered output and test fixtures for it, since nothing else will tell you.

saying these in an interview costs you the question

  • Expects Parse to catch every escaping error
  • Thinks ZgotmplZ in the output means Execute failed
  • Adds templates with Parse after the set was executed
  • Never renders error or admin pages in tests
  • Logs the error bare and loses the template name and line