Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

JavaScript String Format: Template Literals and the Cases They Do Not Cover

Sean

Platform Writer

Aug 20, 2026
8 min read

Template literals use backticks and interpolate any expression with a dollar sign and braces. They replace string concatenation entirely — and they are the wrong tool for numbers, dates, currency, pluralisation, and anything going into HTML or SQL.

JavaScript String Format: Template Literals and the Cases They Do Not Cover

JavaScript has no printf. What it has instead is interpolation plus a genuinely good internationalisation API that most people never open. Between them they cover everything, but the boundary between the two is where formatting bugs live.

Table of contents

Template literals, including the parts people miss

The basics need little explanation:

const name = 'Sam';
const items = 3;

console.log(`${name} has ${items} item${items === 1 ? '' : 's'}`);
// Sam has 3 items

Multi-line strings work without escapes, which is the feature that quietly changed how everyone writes SQL and HTML in JavaScript:

const query = `
  SELECT id, email
  FROM users
  WHERE created_at > $1
  ORDER BY created_at DESC
`;

Note that the leading newline and indentation are part of the string. For output where that matters, either dedent deliberately or accept it — but be aware it is there.

The genuinely underused feature is tagged templates. A function placed before the backticks receives the literal string pieces and the interpolated values separately, which lets it do something with each value before assembling the result:

function money(strings, ...values) {
  return strings.reduce((out, str, i) => {
    if (i >= values.length) return out + str;
    const v = typeof values[i] === 'number'
      ? values[i].toFixed(2)
      : values[i];
    return out + str + v;
  }, '');
}

console.log(money`Total: ${19.5} for ${3} items`);
// Total: 19.50 for 3 items

That separation — literals in one array, values in another — is what makes tagged templates the right foundation for escaping. The tag can see exactly which parts came from a variable, which is precisely the information a naive template lacks.

Numbers, currency and dates belong to Intl

Formatting a number by hand produces something that is right in your locale and wrong in half the world. Intl is built into every runtime and gets this right.

// Thousands separators, per locale
new Intl.NumberFormat('en-US').format(1234567.891);  // 1,234,567.891
new Intl.NumberFormat('de-DE').format(1234567.891);  // 1.234.567,891

// Currency -- symbol placement and decimals handled per currency
new Intl.NumberFormat('en-GB', {
  style: 'currency',
  currency: 'GBP'
}).format(19.5);   // £19.50

new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY'
}).format(1950);   // ¥1,950  -- note: no decimal places

// Percentages
new Intl.NumberFormat('en-US', { style: 'percent' }).format(0.157);  // 16%

// Compact notation
new Intl.NumberFormat('en-US', { notation: 'compact' }).format(1234567);  // 1.2M

The yen example shows why this matters. Not every currency has two decimal places, and a hardcoded toFixed(2) produces nonsense for the ones that do not.

Dates are worse to do by hand and equally well covered:

const d = new Date('2026-03-15T14:30:00Z');

new Intl.DateTimeFormat('en-GB', {
  dateStyle: 'long',
  timeStyle: 'short',
  timeZone: 'Europe/London'
}).format(d);   // 15 March 2026 at 14:30

// Relative time, without a library
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day');   // yesterday
rtf.format(3, 'hour');   // in 3 hours

And pluralisation, which the ternary at the top of this article gets wrong for most languages:

const pr = new Intl.PluralRules('en-US');
const forms = { one: 'item', other: 'items' };

function label(n) {
  return `${n} ${forms[pr.select(n)]}`;
}

label(0);  // 0 items
label(1);  // 1 item
label(2);  // 2 items

English has two plural forms. Russian has four. A ternary encodes an assumption about English that stops being true the moment anyone translates the interface.

Do not use template literals for HTML or SQL

This is where formatting stops being cosmetic. A template literal performs no escaping — it concatenates. Interpolating user input into HTML or SQL is the classic injection vulnerability, dressed in newer syntax.

// Cross-site scripting, waiting to happen
el.innerHTML = `<div class="comment">${userComment}</div>`;

// SQL injection, in modern clothes
db.query(`SELECT * FROM users WHERE email = '${email}'`);

The second one is not made safer by the query being in backticks. It is exactly the concatenation everyone was warned about.

The fixes are the same as they have always been:

// HTML: let the DOM do the escaping
const div = document.createElement('div');
div.className = 'comment';
div.textContent = userComment;   // text, never parsed as markup
el.append(div);

// SQL: parameterised query -- the driver handles quoting
db.query('SELECT * FROM users WHERE email = $1', [email]);

textContent is the whole answer on the HTML side. It inserts text as text, so a comment containing a script tag renders as visible characters rather than executing.

If you must build markup as a string, use a tagged template that escapes interpolated values — which is exactly what the templating in modern frameworks does under the hood, and why they are safe by default while raw innerHTML is not.

Padding, truncation and the character-count problem

For aligned console output or fixed-width formats, the built-ins are adequate:

'5'.padStart(3, '0');        // 005
'name'.padEnd(12, ' ');      // name
'8'.padStart(2, '0') + ':' + '5'.padStart(2, '0');  // 08:05

Truncation is where a subtle bug lives. JavaScript strings are sequences of UTF-16 code units, not characters, so slice can cut a character in half.

const s = 'family: 👨‍👩‍👧‍👦 done';
s.length;          // 20 -- not the number of visible characters
s.slice(0, 9);     // may end mid-emoji, producing a broken glyph

For anything user-facing, count graphemes rather than code units:

function truncate(str, max) {
  const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
  const chars = [...seg.segment(str)].map(s => s.segment);
  return chars.length <= max
    ? str
    : chars.slice(0, max - 1).join('') + '…';
}

This matters more than it sounds for anything with a character limit shown to a user. A counter based on .length tells someone they have used twenty characters when they typed five emoji, which reads as a bug because it is one.

How this fits the rest of the stack

The pattern across all of this is that formatting is a presentation concern with correctness consequences — a currency with the wrong decimals is wrong, a plural rule that assumes English is wrong, and an interpolated string in a query is a vulnerability. The built-ins handle nearly all of it once you know they are there.

Where formatting becomes a deployment concern is locale and timezone: code that formats correctly on your machine can format differently on a server with a different default. Being able to see what the server actually rendered is what turns that into a five-minute fix. On RunxBuild, a service in Node, Next.js, Python, Go, Ruby, Java, .NET or Docker deploys from your GitHub repository with runtime logs and build logs in the same place, environment variables as service settings, and rollback to the previous deploy. Managed MySQL and Postgres sit beside it on private networking. To see what a service, its database and storage come to, the RunxBuild hosting calculator lists them as separate line items.

Useful related references:

FAQ

How do I format a string with variables in JavaScript?

Use a template literal: backticks around the string, with expressions inside a dollar sign and braces. Any expression works, not just variables, and multi-line strings need no escaping. This replaces string concatenation entirely for ordinary text assembly.

How do I format a number as currency in JavaScript?

Use Intl.NumberFormat with style: 'currency' and a currency code. It handles symbol placement, separators and the correct number of decimal places per currency — Japanese yen has none, so a hardcoded two-decimal format produces incorrect output.

Is it safe to build HTML with template literals?

No. Template literals perform no escaping, so interpolating user input into innerHTML is a cross-site scripting vulnerability. Create the element and set textContent, which inserts text as text. The same applies to SQL: use parameterised queries rather than interpolating values into the statement.

What are tagged template literals for?

A function placed before the backticks receives the literal string parts and the interpolated values as separate arguments, so it can process each value before assembling the result. That separation is what makes safe escaping possible, since the tag can tell exactly which parts came from variables.

Why does my string length not match the visible characters?

Because JavaScript strings are sequences of UTF-16 code units, so emoji and combined characters count as more than one. Slicing by index can split a character and produce a broken glyph. Use Intl.Segmenter with grapheme granularity to count and truncate what a user actually sees.

#javascript string format#javascript#template literals#intl#formatting