How to Handle Merge Variables in Email Templates Safely
A null merge variable sends 'Hi ,' to a real customer. Here is how to handle merge variables in email templates safely with defaults, guards, and escaping.
The moment a customer opens an email that says "Hi ," or worse, "Hi {{first_name}}," you have shipped a bug to their inbox, and unlike a web bug, you cannot take it back. Handling merge variables in email templates safely means every variable has a fallback, missing data degrades gracefully, user-provided values are escaped, and you test the ugly cases before you send. Merge variables are where transactional email quietly breaks, because the template looks perfect with tidy sample data and falls apart on the real, messy record.
Why do merge variables break so often?
Because sample data lies. When you build the template, you preview it with first_name: "Sarah" and everything looks great. In production you hit the user who never filled in a name, the company field that is null, the order with a missing line item. The variable that was always "Sarah" in testing is now empty, undefined, or absent, and your template renders the gap.
Two failure shapes show up. The first is the empty render: "Hi ," where the name should be, which looks careless. The second is the raw template leak: "Hi {{first_name}}," where the templating engine failed to substitute at all and printed its own syntax to the customer. Both are public, both are permanent, and both come from the same root cause: trusting that the data will be there.
How do I give every variable a fallback?
Default at the point of substitution. Every merge field gets a sensible fallback so a missing value never renders as a blank or raw syntax. {{first_name | default: "there"}} turns a null name into "Hi there," which is fine, instead of "Hi ," which is not. Do this for every field the user might not have filled in, which in practice is most of them.
Design the copy so it reads correctly with the fallback. "Hi there" works as a greeting; a fallback of "customer" might read cold. The best templates are written so that even the fallback path produces a natural sentence, which means thinking about the empty case while you write, not after a customer reports it. This is the same defensive posture I bring to localizing transactional emails: always have a graceful degradation, never a raw key or a blank.
How do I handle missing sections, not just missing words?
Some data gaps are structural, not just a missing word. An order confirmation with no shipping section because it is a digital product. A summary with zero items. If your template assumes those sections always exist, it renders an empty box, a dangling label, or broken markup.
Guard whole blocks with conditionals: render the shipping section only if there is a shipping address, render the item loop only if there are items, show a zero-state line if the count is zero. The template should handle the full range of real records, including the sparse and empty ones, without rendering a hollow shell. This is exactly the kind of case you have to force in testing, which is why I test transactional emails before shipping against null, empty, and zero-state data on purpose, not just the happy path.
How do I keep merge variables from becoming a security hole?
Escape user-provided values. Merge fields often carry data the user typed: a name, a company, a note, a project title. If you drop that straight into HTML without escaping, a value containing markup can break your layout, and in the worst case inject content you did not intend. Treat every user-sourced merge value as untrusted and escape it for the context it lands in.
Be especially careful with values that flow into links or headers. A user-controlled value interpolated into a URL or a subject line is a place where sloppy substitution turns into a real problem. Escape and validate, and never let raw user input reach a sensitive part of the message unfiltered. Keeping templates in code helps here, because escaped-by-default templating and code review catch these before they ship, which is one more reason I keep transactional email templates in code rather than in a vendor editor where the escaping behavior is opaque.
Test the ugly data, every time
The fix for all of this is refusing to trust the data. Build a set of deliberately broken fixtures: null names, empty companies, missing sections, zero-count lists, absurdly long values, and user values full of special characters. Render the template against all of them in your pipeline, and fail the build if any produce a blank, a raw variable, or broken markup. A template that survives your ugliest fixtures survives production.
Send the tested result through a provider built for developers so the substitution, the payload, and the delivery events all live where you work. Usermails is where I route application email, with an API that fits straight into a tested build pipeline. Default every variable, guard every optional section, escape every user value, and test the empty cases, and your customers stop receiving "Hi ,".