Managing Logic in Handlebars: When to Use Custom Helpers
Stop fighting Handlebars' logic-less constraints. Learn how to use custom simple and block helpers to handle formatting and conditional rendering while avoiding XSS and double-escaping bugs.
03 Nov 2025, 18:26 UTC

The Logic-Less Dilemma
Handlebars is designed as a "logic-less" templating engine. This philosophy forces a strict separation: your data should be prepared in JavaScript before it ever hits the template. However, real-world requirements—like formatting currency, date localization, or conditional CSS classes—often clash with this restriction. When you find yourself wishing you could write a complex if/else chain inside your HTML, you have reached the limit of built-in helpers.
The solution is the Custom Helper. Instead of trying to force logic into the template, you register a JavaScript function that Handlebars calls during execution. This keeps your templates clean and your business logic unit-testable.
Simple Helpers vs. Block Helpers
Handlebars provides two primary ways to extend functionality depending on whether you need a value or a structural change.
- Simple Helpers: These act like functions that return a string. They are ideal for formatting. For example, a
toUpperCasehelper takes a string and returns it in all caps. - Block Helpers: These allow you to wrap a section of the template. They receive an
optionsobject containingfn(the block's content) andinverse(the{{else}}block). This is how the built-in#ifand#eachwork.
The Escaping Trap: SafeString and Triple-Stashes
By default, Handlebars escapes all values to prevent Cross-Site Scripting (XSS) attacks. If your helper returns <strong>Hello</strong>, Handlebars will render it as literal text: <strong>Hello</strong>.
To return actual HTML from a helper, you must wrap the result in new Handlebars.SafeString(). Alternatively, the template author can use triple curly braces {{{value}}} to disable escaping for that specific variable. Using SafeString inside the helper is generally preferred as it explicitly marks the output as trusted by the developer.
Worked Example: A Dynamic Price Formatter
In this example, we assume Handlebars v4.7.x. We will create a helper that formats a number based on a provided currency code and a block helper that highlights "Sale" items.
const Handlebars = require('handlebars');
// 1. Simple Helper: Formats currency
Handlebars.registerHelper('formatCurrency', function(amount, currency = 'USD') {
const formats = {
'USD': new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }),
'EUR': new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }),
};
const formatter = formats[currency] || formats['USD'];
return formatter.format(amount);
});
// 2. Block Helper: Wraps content in a 'sale' badge if the item is discounted
Handlebars.registerHelper('isSale', function(item, options) {
if (item.discount > 0) {
// Return the block content wrapped in HTML
return new Handlebars.SafeString(`<span class="badge">SALE! ${options.fn(this)}</span>`);
}
// Return the 'else' block or nothing
return options.inverse(this);
});
const source = `
{{#each products}}
<div class="item">
<h3>{{name}}</h3>
{{#isSale this}}
<p>Price: {{formatCurrency price currency}}</p>
{{else}}
<p>Price: {{formatCurrency price currency}}</p>
{{/isSale}}
</div>
{{/each}}
`;
const template = Handlebars.compile(source);
const data = {
products: [
{ name: 'Widget A', price: 25.00, currency: 'USD', discount: 10 },
{ name: 'Widget B', price: 15.00, currency: 'EUR', discount: 0 },
]
};
console.log(template(data));
Performance: Precompilation Trade-offs
Compiling templates at runtime using Handlebars.compile() is convenient for development but slow for production because the browser must parse the template string. To optimize, use the Handlebars CLI to precompile templates into JavaScript functions.
The Trade-off: Precompiled templates significantly reduce the client-side payload and CPU usage (you can use handlebars.runtime.js instead of the full library). However, you must rebuild your assets every time a template changes, and you must still register your custom helpers in the browser before executing the precompiled functions.
Verification and Limitations
To verify your helpers are working correctly, check the rendered HTML source in your browser. If you see < instead of <, you have a double-escaping issue—ensure you are using SafeString in your helper return value.
Limitations: Custom helpers cannot modify the data context permanently; they can only transform the output. If you find yourself writing helpers that perform heavy data manipulation or API calls, move that logic into a data-transformation layer (a "ViewModel") before passing the object to the template.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.