date-fns/date-fns · error · RangeError

Format string contains an unescaped latin alphabet character

Error message

Format string contains an unescaped latin alphabet character `${firstCharacter}`

What it means

In format/index.ts:401-407, after the regex splits the format string into runs, any remaining single letter that matches [a-zA-Z] and has no registered formatter throws — date-fns refuses to silently emit an unknown token. Literal letters must be wrapped in single quotes so they are treated as escaped text rather than tokens.

Source

Thrown at pkgs/core/src/format/index.ts:402

    .join("")
    .match(formattingTokensRegExp)!
    .map((substring) => {
      // Replace two single quote characters with one single quote character
      if (substring === "''") {
        return { isToken: false, value: "'" };
      }

      const firstCharacter = substring[0];
      if (firstCharacter === "'") {
        return { isToken: false, value: cleanEscapedString(substring) };
      }

      if (formatters[firstCharacter]) {
        return { isToken: true, value: substring };
      }

      if (firstCharacter.match(unescapedLatinCharacterRegExp)) {
        throw new RangeError(
          "Format string contains an unescaped latin alphabet character `" +
            firstCharacter +
            "`",
        );
      }

      return { isToken: false, value: substring };
    });

  // invoke localize preprocessor (only for french locales at the moment)
  if (locale.localize.preprocessor) {
    parts = locale.localize.preprocessor(originalDate, parts);
  }

  const formatterOptions = {
    firstWeekContainsDate,
    weekStartsOn,
    locale,

View on GitHub (pinned to 4098115cf7)

Solutions

  1. Wrap literal text in single quotes: 'UTC' becomes 'UTC' -> format(date, "yyyy-MM-dd'T'HH:mm 'UTC'").
  2. Use two single quotes '' to represent one literal single quote inside escaped text.
  3. If the letter is meant to be a token, check the Unicode token table and use the correct case (e.g. 'S' for fractions of a second, 'X'/'x' for unix offsets).

Example fix

// before
format(date, 'yyyy-MM-dd HH:mm UTC')
// after
format(date, "yyyy-MM-dd HH:mm 'UTC'")
Defensive patterns

Strategy: validation

Validate before calling

function assertNoUnescapedLatin(fmt: string) {
  const re = /(\w)\1*|''|'(''|[^'])+('|$)|./g
  for (const tok of fmt.match(re) ?? []) {
    if (tok[0] === "'" || !/[a-zA-Z]/.test(tok[0])) continue
    if (!['y','M','d','H','h','m','s','S','a','p','P','E','G','u','w','W','k','K','D','Y','x','X','T','R','Q','q','i','I','L','l','c','N','n','o','A','b','B','v','V','V','Z','z'].includes(tok[0])) {
      throw new Error(`Unescaped latin character: ${tok[0]} in ${fmt}`)
    }
  }
}

Try / catch

try {
  return format(date, fmt)
} catch (e) {
  if (e instanceof RangeError && /unescaped latin alphabet/.test(e.message)) {
    return format(date, fmt.replace(/[A-Za-z]+/g, (m) => `'${m}'`))
  }
  throw e
}

Prevention

When it happens

Trigger: format(date, 'yyyy-MM-dd HH:mm:ss UTC') — the literal 'U','T','C' aren't known tokens. Also format(date, 'Year yyyy') where 'Y','e','a','r' are misread as potential tokens. Anything like 'h:mm a ET' triggers it.

Common situations: Adding timezone abbreviations, timezone offsets, weekday words, or unit labels (e.g. 'min','hrs') as literal text without escaping; refactor that concatenated a format string with variable words.

Related errors


AI-assisted analysis of date-fns/date-fns@4098115cf7 (2026-08-03). Data as JSON: /data/errors/5402aa9b911f908b.json. Report an issue: GitHub.