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 = true

View on GitHub (pinned to 6c01d35af5)

Solutions

  1. Read the details in the message: fix each reported prefix/suffix so open and close delimiter counts match (e.g. '{{x}' -> '{{x}}').
  2. Escape literal braces that are not template variables, or remove stray single braces from the CSS.
  3. Check any upstream template engine output for truncated substitutions before passing it to mjml2html.
  4. 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

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.