mjmlio/mjml · error · Error
Unbalanced template delimiters found in CSS: ${details}. ${g
Error message
Unbalanced template delimiters found in CSS: ${details}. ${getTemplateDelimiterRecoveryMessage(contextName)} What it means
sanitizeTemplateVariablesInHtml scans CSS for template delimiters (e.g. {{ }}) before mjml-core processes template variables. If it finds delimiters whose opening and closing counts are unbalanced (e.g. '{{x}' or '{{x}}}') it throws with the specific broken delimiters and counts, plus a recovery message naming the context. The library refuses to guess how to sanitize CSS with malformed interpolation syntax.
Source
Thrown at packages/mjml-core/src/index.js:401
content: html,
didSanitize: false,
variableMap: {},
propMap: {},
isBlockVariable: false,
}
if (!shouldSanitize) {
return result
}
const broken = detectBrokenTemplateDelimitersInCss(html, syntaxes)
if (broken.length) {
const details = broken
.map(
(b) => `${b.prefix}…${b.suffix} (${b.prefixCount} open, ${b.suffixCount} close)`,
)
.join(', ')
throw new Error(
`Unbalanced template delimiters found in CSS: ${details}. ${getTemplateDelimiterRecoveryMessage(contextName)}`,
)
}
const detected = detectVariableTypeInHtml(html, syntaxes)
result.isBlockVariable = detected.isBlockVariable
if (!allowMixedSyntax && result.isBlockVariable && (detected.isValueVariable || detected.isPropertyVariable)) {
throw new Error(
'Mixed variable syntax detected. Use either CSS property syntax (e.g., color: {{variable}}) OR block syntax (e.g., {{variable}}), not both in the same document.',
)
}
if (detected.isValueVariable) {
const sanitized = sanitizeCssValueVariablesHtml(html, syntaxes)
result.content = sanitized.result
result.variableMap = sanitized.variableMap
result.didSanitize = trueView on GitHub (pinned to 6c01d35af5)
Solutions
- Read the details in the message: fix each reported prefix/suffix so open and close delimiter counts match (e.g. '{{x}' -> '{{x}}').
- Escape literal braces that are not template variables, or remove stray single braces from the CSS.
- Check any upstream template engine output for truncated substitutions before passing it to mjml2html.
- If pre/post delimiters are misconfigured, align the syntaxes option with the delimiters actually used in the CSS.
Example fix
/* before */
color: {{primaryColor};
/* after */
color: {{primaryColor}}; Defensive patterns
Strategy: validation
Validate before calling
function assertBalancedDelimiters(css, open = '{{', close = '}}') {
const o = css.split(open).length - 1;
const c = css.split(close).length - 1;
if (o !== c) throw new Error(`Unbalanced ${open}${close}: ${o} open vs ${c} close`);
} Try / catch
try {
const result = sanitizeTemplateVariablesInHtml(html, opts)
} catch (e) {
if (e.message.startsWith('Unbalanced template delimiters')) {
// surface the broken delimiters to the template author
console.error(e.message)
} else throw e
} Prevention
- Lint templates for stray single braces before builds.
- Escape literal braces that are not template variables.
- Test template rendering in CI to catch delimiter typos early.
When it happens
Trigger: Calling sanitizeTemplateVariablesInHtml (directly or via sanitizationResult) with HTML/CSS containing an odd number of '{{' or '}}' occurrences, such as '{{color}}}' or a single '{' prefix like '{variable}' inside a CSS block.
Common situations: Hand-written email templates with typos in handlebars/Mustache placeholders; CSS inlined into <mj-style> containing literal braces that were meant to be escaped; template engines generating partial interpolation after a failed substitution.
Related errors
AI-assisted analysis of mjmlio/mjml@6c01d35af5 (2026-09-02).
Data as JSON: /api/errors/6b91afaece31ee03.
Report an issue: GitHub.