A template helper errors mid-Execute after bytes reached the ResponseWriter — how do you avoid half-rendered pages?
answer
- Execute writes as it walks
- the first body byte commits the status
- a truncated document can look finished
- render somewhere you can discard
- check the error before copying out
basics
~20 sExecute writes as it goes, so a helper that fails halfway leaves valid-looking output already sent under a 200 status. Render into a bytes.Buffer first, check the error from Execute, and only copy the buffer to the response writer once it returned nil.
solid answer
~50 s`Execute` streams: it writes each action's output to the writer as it reaches it, so an error from a helper at line 40 arrives after lines 1-39 are already gone. On an `http.ResponseWriter` the first write also commits a 200 status and the headers, so you cannot even switch to a 500 afterwards — the client gets a truncated invoice that looks complete and no error anywhere. The fix is to render into a `bytes.Buffer` (or a pooled buffer for large documents), inspect the error from `Execute`, and only then copy the buffer to the response with `buf.WriteTo(w)`. That buys you an all-or-nothing render at the cost of holding the whole document in memory. For genuinely huge documents where buffering is not affordable, the alternative is to make helpers total — resolve everything that can fail before rendering starts and pass it in the data — so the render itself has no failure path left.
code
go · 9 linesvar buf bytes.Buffer
if err := tmpl.Execute(&buf, invoice); err != nil {
http.Error(w, "could not render invoice", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if _, err := buf.WriteTo(w); err != nil {
log.Printf("write invoice: %v", err)
}go deeper
Know that rendering writes output as it goes, so an error partway through does not undo what was already written. Always check the error that Execute returns.
Explain the mechanics: streaming output plus the fact that the first body byte commits the 200 status and headers, which is why the status can no longer be changed when the error arrives.
Present buffer-first as the default and be explicit about its cost in memory per in-flight request, then describe the alternative for large documents — resolving fallible work before the render so helpers cannot fail.
Own the policy: which documents must be all-or-nothing, what a helper is allowed to do on a miss, and how a truncated-but-plausible artefact would ever be noticed in production.
## What actually happens `tmpl.Execute(w, data)` walks the parse tree and writes as it walks. There is no internal buffering and no two-phase commit. So when a helper registered in the func map returns a non-nil error at the fortieth line of an invoice, three things are already true: 1. The first thirty-nine lines have been written to `w`. 2. If `w` is an `http.ResponseWriter`, that first write implicitly sent `200 OK` and the headers. `WriteHeader` afterwards is too late; the runtime logs `superfluous response.WriteHeader call` and ignores it. 3. Your handler now holds an error it cannot turn into an HTTP status. The pathology is worse for documents than for pages, because a truncated document often *looks* finished. An invoice cut off after the line items, before the totals block, renders as a plausible page — the reader has no way to tell that a currency lookup failed. Nobody files a bug; the number is simply missing. ## The buffer-first pattern The standard remedy is to render somewhere you are willing to throw away: ```go var buf bytes.Buffer if err := tmpl.Execute(&buf, invoice); err != nil { http.Error(w, "could not render invoice", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if _, err := buf.WriteTo(w); err != nil { log.Printf("write invoice: %v", err) } ``` Now the render either succeeds completely or nothing reaches the client, and you still have the option of a real error status, a fallback page, or a retry. It also lets you set `Content-Length` or compute an ETag, both of which need the finished body. The cost is memory: the whole document lives in the heap for the duration, and under load that is one buffer per in-flight request. For big documents a `sync.Pool` of buffers reduces the churn, with the usual caveat that you must reset a buffer before reuse and should not retain enormous ones. If a document is large enough that buffering it is genuinely unaffordable, the honest answer is not to stream and hope: it is to remove the failure path. ## Removing the failure path A render that cannot fail needs no transaction. In practice this means resolving everything fallible *before* `Execute` runs — look up the currency rates, the customer record, the tax table, put the results in the data value, and let the helpers be total functions over data that is already known good. A helper that only formats what it is given has no error to return. This is usually the better design anyway, because a template is a bad place to discover that a database row is missing. Failures found before rendering can be reported cleanly, retried, or defaulted; failures found during rendering can only abort. ## Diagnosing it after the fact When this shows up in production the symptom is confusing — a page that renders but is missing its tail, no 500 in the metrics, and a log line only if the handler bothered to log the error from `Execute`. The things that make it findable: - Always log the error from `Execute` with the request context; it carries the template name, line, column, action text and helper name, which points straight at the failing helper. - Compare the byte count written with what a complete render should produce, or assert on a known trailing marker in tests. - In tests, render into a buffer and assert both the error and the full output; a streaming test that only checks the prefix will pass on a truncated document. ## What not to do - Do not "fix" it by making helpers swallow errors and return an empty string. That converts a loud failure into a silently wrong document, which is the same bug with the alarm disconnected — the one exception being a helper where a documented fallback value is genuinely correct. - Do not rely on the client noticing. Truncated HTML is often rendered happily by browsers. - Do not call `WriteHeader` after the first body byte and assume it took effect. ## The judgment to state out loud Buffer-first is the default for anything user-facing, because the memory cost is bounded and predictable while the cost of shipping a plausible-but-wrong document is not. Stream only when the document is large, the consumer can detect truncation (a checksum, a trailer, a content length it verifies), and you have driven the fallible work out of the helpers.
- What does calling WriteHeader after Execute has already written some output do?Nothing useful. The first body write already sent 200 and the headers, so a later `WriteHeader` is ignored and the server logs a superfluous WriteHeader call. That is exactly why the error has to be caught before any byte reaches the response writer — once streaming has begun, the status is spent.
- The document is 200 MB. What do you do instead of buffering it?Remove the failure path rather than trying to make streaming transactional. Resolve every fallible lookup before `Execute` runs and put the results in the data, so the helpers become total functions that only format what they are given. If the consumer can detect truncation — a content length it verifies, a trailer, a checksum — streaming becomes safe because a partial write is visibly partial.
- Why not have helpers return an empty string instead of an error?Because it turns an abort into a silently wrong document, which is harder to detect than a failed request. A blank total on an invoice reaches the customer with no error anywhere. Swallowing is only right where a documented fallback is genuinely correct — a missing translation rendering as the key, say — and then it should be logged, not hidden.
- How do you find the failing helper from the error Execute returned?Log the error itself rather than a summary. It carries the template name, the line and column of the action, the action text and the helper's name, so it points straight at the call. Because the helper's error is wrapped, `errors.Is` and `errors.As` still work, which lets the handler distinguish an expected miss from a genuine bug.
saying these in an interview costs you the question
- Believes Execute buffers internally and writes at the end
- Tries to set a 500 status after output was written
- Makes helpers return empty strings to avoid aborting
- Assumes a truncated page is visibly broken to users
- Ignores the error returned by Execute entirely
- Thinks a deferred recover can undo bytes already sent