toon-format/toon · error · SyntaxError

Invalid escape sequence: \u${hex} is a lone surrogate. Suppl

Error message

Invalid escape sequence: \u${hex} is a lone surrogate. Supplementary code points MUST appear as literal UTF-8

What it means

TOON deliberately forbids \u escapes in the surrogate range D800–DFFF (lone surrogates). Supplementary code points must be written as literal UTF-8 characters. This avoids producing invalid strings and normalizes representation.

Source

Thrown at packages/toon/src/shared/string-utils.ts:94

        i += 2
        continue
      }
      if (next === DOUBLE_QUOTE) {
        unescaped += DOUBLE_QUOTE
        i += 2
        continue
      }
      if (next === 'u') {
        if (i + 6 > value.length) {
          throw new SyntaxError(`Invalid escape sequence: truncated \\u escape at "${value.slice(i, i + 6)}"`)
        }
        const hex = value.slice(i + 2, i + 6)
        if (!/^[0-9a-f]{4}$/i.test(hex)) {
          throw new SyntaxError(`Invalid escape sequence: \\u must be followed by 4 hex digits, got "${hex}"`)
        }
        const codeUnit = Number.parseInt(hex, 16)
        if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) {
          throw new SyntaxError(`Invalid escape sequence: \\u${hex} is a lone surrogate. Supplementary code points MUST appear as literal UTF-8`)
        }
        unescaped += String.fromCodePoint(codeUnit)
        i += 6
        continue
      }

      throw new SyntaxError(`Invalid escape sequence: \\${next}`)
    }

    unescaped += value[i]
    i++
  }

  return unescaped
}

/** Finds the index of the closing double quote, accounting for escape sequences. */
export function findClosingQuote(content: string, start: number): number {

View on GitHub (pinned to 604eac266e)

Solutions

  1. Replace the surrogate-pair escape with the literal UTF-8 emoji/character (e.g. 😀)
  2. Re-encode the source using a converter that emits UTF-8 instead of surrogate escapes
  3. Combine surrogate pairs correctly before conversion if migrating from JSON

Example fix

// before (TOON input)
emoji: "\uD83D\uDE00"
// after
emoji: "😀"
Defensive patterns

Strategy: validation

Validate before calling

function hasLoneSurrogateEscapes(s: string): boolean {
  const hexes = [...s.matchAll(/\\u([dD][89abAB][0-9a-fA-F]{2}|[dD][cdefCDEF][0-9a-fA-F]{2})/g)].map(m => parseInt(m[1], 16))
  return hexes.some(h => h >= 0xD800 && h <= 0xDFFF)
}

Try / catch

try {
  const value = decode(input)
} catch (e) {
  if (e instanceof SyntaxError && e.message.includes('lone surrogate')) {
    // convert surrogate pairs to real characters before decoding
  }
  throw e
}

Prevention

When it happens

Trigger: Parsing a TOON string containing \uD800–\uDFFF (e.g. "\uD83D" without its low surrogate, or any lone surrogate code).

Common situations: Converting JSON that used surrogate pairs (\uD83D\uDE00) into TOON; tools that escape astral characters as surrogate pairs rather than emitting UTF-8.

Related errors


AI-assisted analysis of toon-format/toon@604eac266e (2026-08-31). Data as JSON: /api/errors/18bc96c27689378a. Report an issue: GitHub.