Your First Templ Component and the Code It Generates
Write a first templ component and read the Go it generates: string chunks, escaped expressions, error positions and how a failed render behaves over HTTP.

The fastest way to stop being wary of a code generator is to read what it writes. templ is pleasantly unmagical in that respect: the output is ordinary Go with no reflection, no runtime parsing and a shape you can follow in a debugger. This article writes one small component, runs the generator, goes through the interesting lines of the result, and then makes a render fail on purpose to see how errors behave.
Tested with Go 1.25.6 and templ v0.3.1001, macOS on Apple Silicon (arm64)
It is the second article in the templ series, after the tutorial, and the groundwork for layouts and composition and escaping. It belongs to the web cluster that ends in a production HTTP server. Everything ran on Go 1.25.6 with templ v0.3.1001.
The component
Two components in one file. Greeting has a parameter, an interpolation and a condition. Record calls a Go function that can fail.
package main
import (
"errors"
"fmt"
)
func lookup(n int) (string, error) {
if n < 0 {
return "", errors.New("negative id")
}
return fmt.Sprintf("record-%d", n), nil
}
templ Greeting(name string, visits int) {
<p class="greeting">
Hello, { name }!
if visits > 1 {
<span>Welcome back, visit { fmt.Sprint(visits) }.</span>
}
</p>
}
templ Record(n int) {
<div>{ lookup(n) }</div>
}Everything before the templ keyword is ordinary Go: the package clause, imports and helper functions live in the same file and are copied into the generated one. lookup returns (string, error), and templ lets an expression in braces be exactly that shape; if the error is non-nil, rendering stops and the error comes back to the caller.
What the generator wrote
The program below renders each component into a string and prints the result and the error. It also puts one failing component behind templ.Handler, and builds a component by hand.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"github.com/a-h/templ"
)
// A component does not have to come from a .templ file: anything with Render is one.
func shout(s string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, "<b>"+templ.EscapeString(strings.ToUpper(s))+"</b>")
return err
})
}
func show(label string, c templ.Component) {
var sb strings.Builder
err := c.Render(context.Background(), &sb)
fmt.Printf("%-24s html=%q\n%-24s err=%v\n", label, sb.String(), "", err)
}
func main() {
show("Greeting, first visit", Greeting("Mara", 1))
show("Greeting, markup in name", Greeting("<i>Mara</i>", 3))
show("Record 7", Record(7))
show("Record -1 (fails)", Record(-1))
show("a hand-written component", shout("go & html"))
// The same failing component behind an HTTP handler.
rec := httptest.NewRecorder()
templ.Handler(Record(-1)).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
fmt.Printf("\ntempl.Handler on failure: status %d, body %q\n", rec.Code, rec.Body.String())
rec = httptest.NewRecorder()
handler := templ.Handler(Record(-1), templ.WithErrorHandler(func(r *http.Request, err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "something broke", http.StatusInternalServerError)
})
}))
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
fmt.Printf("with WithErrorHandler: status %d, body %q\n", rec.Code, strings.TrimSpace(rec.Body.String()))
}The commands that follow generate the Go file, print its size, list the lines that show how the markup is handled, and run the program.
go get github.com/a-h/[email protected] >/dev/null 2>&1
go get -tool github.com/a-h/templ/cmd/[email protected] >/dev/null 2>&1
go tool templ generate >/dev/null 2>&1
echo "hello.templ: $(wc -l < hello.templ) lines, hello_templ.go: $(wc -l < hello_templ.go) lines"
grep -n 'WriteString(templ_7745c5c3_Buffer\|EscapeString\|templ.Error' hello_templ.go | cut -c1-170
echo "---"
go run .hello.templ: 26 lines, hello_templ.go: 130 lines
44: templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<p class=\"greeting\">Hello, ")
51: return templ.Error{Err: templ_7745c5c3_Err, FileName: `hello.templ`, Line: 17, Col: 15}
53: _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
57: templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "! ")
62: templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<span>Welcome back, visit ")
69: return templ.Error{Err: templ_7745c5c3_Err, FileName: `hello.templ`, Line: 19, Col: 49}
71: _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
75: templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, ".</span>")
80: templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p>")
109: templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div>")
116: return templ.Error{Err: templ_7745c5c3_Err, FileName: `hello.templ`, Line: 25, Col: 17}
118: _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
122: templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
---
Greeting, first visit html="<p class=\"greeting\">Hello, Mara! </p>"
err=<nil>
Greeting, markup in name html="<p class=\"greeting\">Hello, <i>Mara</i>! <span>Welcome back, visit 3.</span></p>"
err=<nil>
Record 7 html="<div>record-7</div>"
err=<nil>
Record -1 (fails) html="<div>"
err=hello.templ: error at line 25, col 17: negative id
a hand-written component html="<b>GO & HTML</b>"
err=<nil>
templ.Handler on failure: status 500, body "templ: failed to render template\n"
with WithErrorHandler: status 500, body "something broke"The size comes first: 26 lines of templ became 130 lines of Go, most of it the repeated shape of "write a chunk, check the error". That ratio is typical, and it is the reason to treat the generated file as an artifact, not as source.
The interesting lines are the filtered ones. Each stretch of static markup is a call to templruntime.WriteString with an increasing index and a constant string: <p class="greeting">Hello, , then ! , then <span>Welcome back, visit , then .</span> and </p>. Nothing about the static parts is computed at request time. The markup was split at every point where something dynamic or conditional happens, so each chunk is written exactly once, as a constant, with no allocation. The index is bookkeeping for watch mode, which lets the dev server swap chunks without a rebuild.
Every dynamic value follows the same two steps. The value goes through templ.EscapeString, whose result is what gets written; that call is how the guarantee "expressions are escaped" is implemented, and it is why a component author cannot forget it. Every expression failure is wrapped as templ.Error{Err: ..., FileName: hello.templ, Line: 17, Col: 15}, with the position of the { } in your source file, copied into the generated code at generation time. The numbers in the grep output (lines 17, 19 and 25) are lines of hello.templ, not of the generated file.
How rendering fails
The program's output shows the two behaviors. The first component greets Mara, and the <span> appears only when visits is above one. The second call passes <i>Mara</i> as the name and gets <i>Mara</i> back: escaped, as designed. Record(7) renders record-7.
Record(-1) is the failure. The error reads hello.templ: error at line 25, col 17: negative id, which is the position of { lookup(n) } in the template and the message of the error that function returned. That is better than the usual runtime-template experience of "template: x:1:2: executing ... error calling lookup", because it names the file you edit and the exact column.
Look at what the string builder contains after the failure: <div>. The component wrote its opening tag before it evaluated the expression that failed, and those bytes reached the writer. For a unit test that is harmless. For an HTTP response it matters, because once bytes are written the status is 200 and cannot change. The last two lines show how the library handles this for you. templ.Handler rendered the component, hit the error, and answered 500 with a generic message instead of a half page, because the handler buffers the output until the component succeeds (unless you ask for streaming with templ.WithStreaming(), covered in the streaming article). templ.WithErrorHandler lets you supply your own response, which is where you would log the error with its position and render a friendly page. If you call Render directly into a http.ResponseWriter yourself, you lose that buffering: render into a bytes.Buffer first, or accept that an error mid-render may leave a truncated page.
Components are just an interface
templ.Component is an interface with one method, Render(ctx context.Context, w io.Writer) error, and that is the whole contract. The hand-written shout function in main.go builds one with templ.ComponentFunc, an adapter that turns a function into a component. It escaped the text after uppercasing it, which is the right order: uppercase first, escape last, otherwise & becomes &. (My first version of this demo had them the wrong way round, and the output showed &, which is exactly the kind of bug the escape-last rule prevents.) You can use such a function inside a templ file with @shout("go"), since @ calls any expression that evaluates to a component.
That openness is useful in practice. Components that need a loop or an external renderer can be written directly in Go, a templ.Join(a, b, c) concatenates components, templ.Raw("<hr>") wraps trusted HTML, and the same interface is what the testing and fragment features build on.
Cancellation and cost
One more detail in the generated code is the first thing each component does: it checks ctx.Err() and returns it if the context is already cancelled. A client that disconnects while a large page is being built stops the work at the next component boundary instead of rendering to a dead connection. The buffers are pooled and released when the outermost component finishes, so rendering a page does not allocate a fresh buffer per component. The comparison with html/template measures what that adds up to.
Reading the generated file once is enough. Afterwards you can treat it as a build artifact, as you do with the output of go build, and put your attention on the .templ source, which is where all the decisions are made. The reference for the component interface and the helper functions is on pkg.go.dev, and the syntax guide is at templ.guide.


