Why can a Go template's Execute leave half a document in the writer, and how do you avoid shipping that?
answer
- it streams, it cannot rewind
- the error arrives after the bytes
- stage it before you send it
- atomicity costs one copy
- boot, test, buffer — in that order
basics
~10 sExecute streams to the io.Writer as it walks the template, so bytes written before a failing action stay written. Render into a bytes.Buffer first and forward it only when Execute returns nil.
solid answer
~50 s`Execute(w, data)` is a streaming operation: it writes literal text and rendered values to `w` as it walks the parse tree, and it cannot take bytes back. When an action fails partway — a missing struct field, a comparison of mismatched kinds, a failing write — execution stops and the error is returned, but everything written up to that point is already in the writer. In a notification worker that is half a message body; against a network connection it is a truncated payload the peer will try to interpret. The fix is to render into a `bytes.Buffer`, check the error, and only then hand `buf.Bytes()` to whatever sends or persists it. That costs one copy of the document and buys atomicity plus a chance to inspect the output. Reserve direct streaming for genuinely large outputs where truncation is acceptable or detectable.
code
go · 8 linesvar buf bytes.Buffer
if err := t.Execute(&buf, msg); err != nil {
return fmt.Errorf("render %s: %w", msg.ID, err)
}
if bytes.Contains(buf.Bytes(), []byte("<no value>")) {
return fmt.Errorf("render %s: unresolved placeholder", msg.ID)
}
return send(msg.To, buf.Bytes())go deeper
Remember that Execute writes as it goes: an error partway through does not undo what was already written. The safe habit is to render into a bytes.Buffer first.
Explain why streaming is the right default for an engine that may render unbounded output, and name the failures that can stop a render mid-way, such as a missing field or a failing write.
Show the staging pattern with the error checked before the destination is touched, plus what buffering buys beyond atomicity — output inspection, size limits, reuse — and when the memory cost makes streaming the right call.
Set the layering as policy: parse at boot, validate payload shapes in tests, buffer before any destination that cannot be truncated. Then state the exception for large outputs and how truncation is detected there.
## The contract `func (t *Template) Execute(wr io.Writer, data any) error` applies the parsed template to `data` and writes the output to `wr`. The documented behaviour on failure is explicit: execution stops, but **partial results may already have been written to the output writer**. There is no rollback and no internal staging buffer. This follows from the design. A template can render a value of unbounded size — a range over a million rows — so buffering everything internally would be the wrong default. The engine streams, and the caller decides whether atomicity matters. ## What can fail mid-render Execution errors are not exotic; they are the everyday ones: - a field that does not exist on the struct you passed, or is unexported, - a comparison between mismatched kinds, e.g. `eq` on a string and an int, - a map lookup that misses while `missingkey=error` is set, - a method or function invoked by the template returning a non-nil error, - an error from `wr.Write` itself — a closed connection, a full disk. All of these can happen deep inside a `range`, after thousands of bytes are already out. ## Why it hurts in practice Consider a worker that renders and sends notifications. Writing straight to the outbound message body means a failure at item 40 of 50 produces a message that reads as if it were complete up to a cliff. The recipient has no way to tell. Retrying sends a second, also-truncated copy unless the underlying cause changed. And the error your worker logs — `can't evaluate field Plan` — arrives *after* the damage, so error handling cannot undo it, only report it. The same shape appears anywhere the writer is not a scratch space: a file being written in place, a network connection, an audit log. ## The pattern ```go var buf bytes.Buffer if err := t.Execute(&buf, msg); err != nil { return fmt.Errorf("render %s: %w", msg.ID, err) } return send(msg.To, buf.Bytes()) ``` The destination is touched only on success, so it sees a whole document or nothing at all. Two bonuses come free: - **Inspection.** You can assert on the rendered bytes before they leave — a length bound, a check that no placeholder text survived, a content-hash for deduplication. - **Reuse.** The same bytes can go to two destinations without rendering twice. The cost is one full copy of the output in memory. For a message body or an HTML page that is trivially cheap. For a multi-gigabyte report it is not, and there streaming with a truncation marker, or writing to a temporary file and renaming on success, is the better trade. ## Where else this shows up The same reasoning explains why templates should be parsed at startup: a parse error caught at boot never reaches a writer at all, while a parse deferred to first use turns a syntax typo into a runtime failure on a live request. Push each class of failure as early as it will go — parse errors to boot, data-shape errors to a test, and everything left over into a buffer that never reaches the destination. A related habit for long renders is a bounded writer: wrap the buffer so it errors past a size limit, so a runaway `range` over an unexpectedly huge slice fails loudly rather than allocating until the process dies. That converts an availability problem into an ordinary error. ## The interview shape A good answer states the contract from the documentation (partial results may already have been written), names concrete mid-render failures, gives the buffer pattern in two lines, and then — this is the part that separates senior from middle — says when *not* to buffer, and what the memory cost is. Blanket rules are weaker than a stated trade.
- When would you accept streaming a template directly to its destination?When the output is large enough that buffering it is a real memory cost — a big report or export — and a truncated result is either acceptable or independently detectable, for example because the consumer validates a trailer or a length. Even then, writing to a temporary file and renaming on success gives atomicity for a fraction of the memory. The decision is about output size and how the consumer detects truncation.
- How does parsing templates at startup relate to this problem?It removes one class of failure from the write path entirely. A syntax error found by `Parse` during startup stops the process before any writer exists; the same error found lazily on first use fails a live request. The layering is: syntax errors at boot, data-shape errors in tests against representative payloads, and whatever remains contained by rendering into a buffer that never reaches the destination.
- What guards a template render against a range over an unexpectedly huge slice?Wrap the in-memory writer so its Write returns an error past a size limit. `Execute` stops at the first failing write and returns that error, so a runaway render becomes an ordinary handled error instead of unbounded allocation. Combine it with an upstream cap on collection sizes in the payload, since the template is the wrong place to enforce a business limit.
saying these in an interview costs you the question
- Assumes Execute writes nothing when it returns an error
- Thinks checking the returned error prevents partial output
- Renders directly to a destination that cannot be truncated
- Believes the engine buffers internally before writing
- Buffers unconditionally without considering output size